STM32學習——半天學完正點原子入門篇例程,STM32:學會了嗎?我:學廢了✨

2021-09-27 10:00:08

本文程式碼均來正點原子標準例程
宣告:本文不是教學文章,可能也不適合初學者閱讀

不知為什麼,最近總蹦出有很多想法(可能是工作太閒了)一會想學這,一會想學那,這不,突然想複習一下STM32了。

我好久以前就學過正點原子的課程,還買過一些開發板,但現在手上只有一個核心板了,就暫且湊合著用吧。

我是個喜歡制定計劃的人,既然有了想法,那就得制定一個學習計劃,估摸了一下,明天要上班,現在已經中午了,所以我只有一個下午加一個晚上的時間。哎😢,工作之後發現學習的時間太少了,所以,既然是複習,那就不搞那麼多彎彎繞繞了,直接針對正點原子的程式碼,通過程式碼學習STM32,那些啥原理的,通通給我拋到九霄雲外去,以後有機會慢慢整。

開發平臺

話不多說,開始整活,先準備一下硬體:
在這裡插入圖片描述
就一個核心板,太寒酸了,還好有個螢幕撐撐場面。核心板的MCU型號為STM32F103ZET6。

有了硬體,就差程式碼了。
下圖是正點原子的入門篇視訊,我就按照這個順序來學一遍(沒有硬體支援的話,就只能跳過了,如OLED),暫存器版的就不考慮了,太麻煩。
在這裡插入圖片描述
雖然從教學視訊的目錄上看感覺實驗多得有些嚇人,但開啟工程資料夾一看,嘿嘿,舒服了。😁,這麼一點,一下午就能搞完。
在這裡插入圖片描述
就在我竊喜的時候,看了一眼時間,時間不多了,抓緊了🚐。。。

在這裡插入圖片描述

實驗1 跑馬燈實驗

main()

光看主函數,覺得他和51一樣簡單,就是初始化和設定GPIO的高低,但實際上它們有本質區別,畢竟一個是8位元,一個是32位元。下面我們來一行行地分析吧。


int main(void)
{ 
 
	delay_init();		  //初始化延時函數
	LED_Init();		        //初始化LED埠
	while(1)
	{
			GPIO_ResetBits(GPIOB,GPIO_Pin_5);  //LED0對應引腳GPIOB.5拉低,亮  等同LED0=0;
			GPIO_SetBits(GPIOE,GPIO_Pin_5);   //LED1對應引腳GPIOE.5拉高,滅 等同LED1=1;
			delay_ms(300);  		   //延時300ms
			GPIO_SetBits(GPIOB,GPIO_Pin_5);	   //LED0對應引腳GPIOB.5拉高,滅  等同LED0=1;
			GPIO_ResetBits(GPIOE,GPIO_Pin_5); //LED1對應引腳GPIOE.5拉低,亮 等同LED1=0;
			delay_ms(300);                     //延時300ms
	}
} 

delay_init() 函數

//初始化延遲函數
//SYSTICK的時鐘固定為HCLK時鐘的1/8
//SYSCLK:系統時鐘
void delay_init()
{
	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);	//選擇外部時鐘  HCLK/8
	fac_us=SystemCoreClock/8000000;				//為系統時鐘的1/8  
	fac_ms=(u16)fac_us*1000;					//非OS下,代表每個ms需要的systick時鐘數   
}								    

第一個函數delay_init(),不像51裡直接用一個while實現延時,這裡的延時由滴答定時器實現。Systick定時器就是系統滴答定時器,一個24 位的倒計數定時器,計到0 時,將從RELOAD 暫存器中自動重灌載定時初值。只要不把它在SysTick 控制及狀態暫存器中的使能位清除,就永不停息,即使在睡眠模式下也能工作。
SysTick_CLKSourceConfig是一個庫函數,作用是設定滴答定時器的時鐘源。

STM32 有5個時鐘源:HSI、HSE、LSI、LSE、PLL。
①、HSI是高速內部時鐘,RC振盪器,頻率為8MHz,精度不高。
②、HSE是高速外部時鐘,可接石英/陶瓷諧振器,或者接外部時鐘源,頻率範圍為4MHz~16MHz。
③、LSI是低速內部時鐘,RC振盪器,頻率為40kHz,提供低功耗時鐘。WDG
④、LSE是低速外部時鐘,接頻率為32.768kHz的石英晶體。RTC
⑤、PLL為鎖相環倍頻輸出,其時鐘輸入源可選擇為HSI/2、HSE或者HSE/2。
倍頻可選擇為2~16倍,但是其輸出頻率最大不得超過72MHz。

STM32時鐘源的知識還是挺多的,我自己現在也不是很清楚(得專門抽空學學),但我知道如果沒有做設定,系統預設時脈頻率是最高頻率——本平臺為72MHz
system_stm32f10x.c裡有以下內容,先記錄一下,以後再分析。

#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* #define SYSCLK_FREQ_HSE    HSE_VALUE */
 #define SYSCLK_FREQ_24MHz  24000000
#else
/* #define SYSCLK_FREQ_HSE    HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz  24000000 */ 
/* #define SYSCLK_FREQ_36MHz  36000000 */
/* #define SYSCLK_FREQ_48MHz  48000000 */
/* #define SYSCLK_FREQ_56MHz  56000000 */
#define SYSCLK_FREQ_72MHz  72000000
#endif

SysTick_CLKSourceConfig(…)

下面看看滴答定時器時鐘源設定的庫函數原始碼,可以看出它的時鐘源只能為SysTick_CLKSource_HCLK_Div8SysTick_CLKSource_HCLK,那麼問題來了,什麼是HCLK:

HCLK :AHB匯流排時鐘,由系統時鐘SYSCLK 分頻得到,一般不分頻,等於系統時鐘

剛剛提到系統時鐘為72M,所以SysTick_CLKSource_HCLK_Div8 就是72/8=9M。

/**
  * @brief  Configures the SysTick clock source.
  * @param  SysTick_CLKSource: specifies the SysTick clock source.
  *   This parameter can be one of the following values:
  *     @arg SysTick_CLKSource_HCLK_Div8: AHB clock divided by 8 selected as SysTick clock source.
  *     @arg SysTick_CLKSource_HCLK: AHB clock selected as SysTick clock source.
  * @retval None
  */
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource)
{
  /* Check the parameters */
  assert_param(IS_SYSTICK_CLK_SOURCE(SysTick_CLKSource));
  if (SysTick_CLKSource == SysTick_CLKSource_HCLK)
  {
    SysTick->CTRL |= SysTick_CLKSource_HCLK;
  }
  else
  {
    SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
  }
}

設定完了滴答定時器的時鐘,delay_init函數內還有兩行:

fac_us=SystemCoreClock/8000000;	//為系統時鐘的1/8  
fac_ms=(u16)fac_us*1000;		//非OS下,代表每個ms需要的systick時鐘數   		

fac_us表示微秒的計時因子,即滴答計時器過載值為1*fac_us時,計時時間為1us(可以看後面的delay_us函數),fac_ms為fac_us的1000倍,自然就是1ms了。

  • 那麼問題來了,為什麼fac_us代表1us呢?

之前我們提到
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //選擇外部時鐘 HCLK/8
即滴答定時器定時器頻率為9M(72/8),9M意味著定時器1秒計數9000000,那麼1毫秒計數就為9000,1微秒為9。這代表什麼?計9次數為1us,這個9就是1微秒的計數因子(fac_us),即fac_us(72000000/8000000=9)代表1us。n微秒則為n * fac_us。

LED_Init() 函數

終於到了本實驗的主角——LED(GPIO)

//初始化PB5和PE5為輸出口.並使能這兩個口的時鐘		    
//LED IO初始化
void LED_Init(void)
{
 
 GPIO_InitTypeDef  GPIO_InitStructure;
 	
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE, ENABLE);	 //使能PB,PE埠時鐘
	
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;				 //LED0-->PB.5 埠設定
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推輓輸出
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //IO口速度為50MHz
 GPIO_Init(GPIOB, &GPIO_InitStructure);					 //根據設定引數初始化GPIOB.5
 GPIO_SetBits(GPIOB,GPIO_Pin_5);						 //PB.5 輸出高

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;	    		 //LED1-->PE.5 埠設定, 推輓輸出
 GPIO_Init(GPIOE, &GPIO_InitStructure);	  				 //推輓輸出 ,IO口速度為50MHz
 GPIO_SetBits(GPIOE,GPIO_Pin_5); 						 //PE.5 輸出高 
}

概括一下設定GPIO的步驟:

  1. 定義一個GPIO_InitTypeDef 成員
  2. 使能GPIO對應的埠時鐘RCC_APB2PeriphClockCmd(...)
  3. 設定引腳GPIO_Pin
  4. 設定模式(輸入、輸出、推輓、開漏、浮空)GPIO_Mode
  5. 設定IO速度(我練習時一般不太在意這一項)GPIO_Speed
  6. 初始化GPIO_InitTypeDef 成員GPIO_Init(..)
  7. 設定引腳高低狀態GPIO_SetBits(..)GPIO_ResetBits(...)

RCC_APB2PeriphClockCmd(…)

庫函數註釋中標明瞭時鐘匯流排上的外設,GPIOB和GPIOE都在APB2匯流排上

/**
  * @brief  Enables or disables the High Speed APB (APB2) peripheral clock.
  * @param  RCC_APB2Periph: specifies the APB2 peripheral to gates its clock.
  *   This parameter can be any combination of the following values:
  *     @arg RCC_APB2Periph_AFIO, RCC_APB2Periph_GPIOA, RCC_APB2Periph_GPIOB,
  *          RCC_APB2Periph_GPIOC, RCC_APB2Periph_GPIOD, RCC_APB2Periph_GPIOE,
  *          RCC_APB2Periph_GPIOF, RCC_APB2Periph_GPIOG, RCC_APB2Periph_ADC1,
  *          RCC_APB2Periph_ADC2, RCC_APB2Periph_TIM1, RCC_APB2Periph_SPI1,
  *          RCC_APB2Periph_TIM8, RCC_APB2Periph_USART1, RCC_APB2Periph_ADC3,
  *          RCC_APB2Periph_TIM15, RCC_APB2Periph_TIM16, RCC_APB2Periph_TIM17,
  *          RCC_APB2Periph_TIM9, RCC_APB2Periph_TIM10, RCC_APB2Periph_TIM11     
  * @param  NewState: new state of the specified peripheral clock.
  *   This parameter can be: ENABLE or DISABLE.
  * @retval None
  */
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
{
  /* Check the parameters */
  assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
  assert_param(IS_FUNCTIONAL_STATE(NewState));
  if (NewState != DISABLE)
  {
    RCC->APB2ENR |= RCC_APB2Periph;
  }
  else
  {
    RCC->APB2ENR &= ~RCC_APB2Periph;
  }
}

如果想快速查到某外設的時鐘匯流排,可以參考《STM32中文參考手冊》記憶體和匯流排架構章節:
在這裡插入圖片描述

GPIO_InitTypeDef

下面是GPIO_InitTypeDef結構體定義

/** 
  * @brief  GPIO Init structure definition  
  */

typedef struct
{
  uint16_t GPIO_Pin;             /*!< Specifies the GPIO pins to be configured.
                                      This parameter can be any value of @ref GPIO_pins_define */

  GPIOSpeed_TypeDef GPIO_Speed;  /*!< Specifies the speed for the selected pins.
                                      This parameter can be a value of @ref GPIOSpeed_TypeDef */

  GPIOMode_TypeDef GPIO_Mode;    /*!< Specifies the operating mode for the selected pins.
                                      This parameter can be a value of @ref GPIOMode_TypeDef */
}GPIO_InitTypeDef;

LED_init中,LED0GPIO_Pin為GPIOB5,LED1為GPIOE5;
模式都選擇了推輓輸出

推輓輸出的最大特點是可以真正能真正的輸出高電平和低電平,在兩種電平下都具有驅動能力。

由LED的原理圖可以知道它們為共陽極,所以預設要將IO拉高。
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

其他細節感興趣的可以自己去研究😗。

delay_ms(…)函數

fac_ms剛剛在延時函數初始化中已經介紹,滴答定時器SysTick每計時fac_ms次,則表示1ms,所以nms*fac_ms表示計時nms毫秒。SysTick->LOAD為定時器的過載值,SysTick->VAL表示計數值,還要注意:滴答定時器是倒數計數的。SysTick->CTRL為控制暫存器,第16位元可以用來檢測是否倒數到0。
在這裡插入圖片描述

void delay_ms(u16 nms)
{	 		  	  
	u32 temp;		   
	SysTick->LOAD=(u32)nms*fac_ms;				//時間載入(SysTick->LOAD為24bit)
	SysTick->VAL =0x00;							//清空計數器
	SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;	//開始倒數  
	do
	{
		temp=SysTick->CTRL;
	}while((temp&0x01)&&!(temp&(1<<16)));		//等待時間到達   
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;	//關閉計數器
	SysTick->VAL =0X00;       					//清空計數器	  	    
} 

位元運算

對於操作暫存器,經常要用到位元運算,如SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk中,SysTick_CTRL_ENABLE_Msk表示1,SysTick->CTRL|=1的作用是將CTRL暫存器的最低位置1,而不影響其他高19位(0或任何二進位制數,都會是它自己);
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;的作用是將CTRL最低位置0,0x00000001按位元取反後為0xfffffffe,該數與任何32位元數按位元與(&),都不會影響高31位元,因為1和任何二進位制數進行與運算都等於它自己。


本來想寫位帶操作的,但看了看時間,就放棄了

在這裡插入圖片描述


GPIO引腳控制函數就不提了,之前在LED_Init()函數裡已經見過。
實驗效果——紅綠燈交替閃爍。
在這裡插入圖片描述
在這裡插入圖片描述

實驗2 按鍵輸入

main()

主函數中LED0、LED1和BEEP代表的是GPIO的位元欄(本文忽略這個概念),把它當做51裡對GPIO的位元運算就行了。
與上一個實驗相比,本實驗多了按鍵模組和蜂鳴器模組。


int main(void)
 {
 	vu8 key=0;	
	delay_init();	    	 //延時函數初始化	  
 	LED_Init();			     //LED埠初始化
	KEY_Init();          	//初始化與按鍵連線的硬體介面
	BEEP_Init();         	//初始化蜂鳴器埠
	LED0=0;					//先點亮紅燈
	while(1)
	{
 		key=KEY_Scan(0);	//得到鍵值
	   	if(key)
		{						   
			switch(key)
			{				 
				case WKUP_PRES:	//控制LED1翻轉	
					LED1=!LED1;
					BEEP = !BEEP;
					break;
				case KEY0_PRES:	//同時控制LED0翻轉 
					LED0=!LED0;
					BEEP = !BEEP;
					break;
			}
		}else delay_ms(10); 
	}	 
}

KEY_Init()函數

與LED_Init()類似,設定步驟相同(設定步驟見LED_Init()介紹部分)。

void KEY_Init(void) //IO初始化
{ 
 	GPIO_InitTypeDef GPIO_InitStructure;
 
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);//使能PORTA,PORTE時鐘

	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_4;//KEY0
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //設定成下拉輸入
 	GPIO_Init(GPIOE, &GPIO_InitStructure);//初始化GPIOE2,3,4

	//初始化 WK_UP-->GPIOA.0	  下拉輸入
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0設定成輸入,預設下拉	  
	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.0
}

開發板上有兩個按鍵,KEY_UPKEY0,都是一端接高電平,一端接IO,所以模式設定為下拉輸入,KEY_UP對應的GPIO引腳為GPIOA0,KEY0對應的引腳為GPIOE4。IO時鐘都掛載在APB2上。
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

BEEP_Init()函數

開發板上並沒有蜂鳴器,我選擇了外接一個蜂鳴器,同樣接在PB8引腳上。初始化設定步驟和LED與KEY相同,模式為推輓輸出,由於我的蜂鳴器低電平有效,所以初始化中還需把IO電平拉高。

//初始化PB8為輸出口.並使能這個口的時鐘		    
//蜂鳴器初始化
void BEEP_Init(void)
{
 
 GPIO_InitTypeDef  GPIO_InitStructure;
 	
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	 //使能GPIOB埠時鐘
 
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;				 //BEEP-->PB.8 埠設定
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推輓輸出
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	 //速度為50MHz
 GPIO_Init(GPIOB, &GPIO_InitStructure);	 //根據引數初始化GPIOB.8
 
 GPIO_SetBits(GPIOB,GPIO_Pin_8);//輸出0,關閉蜂鳴器輸出

}

KEY_Scan(…)函數

該函數中,mode表示模式,為0表示短按,為1表示長按。區域性靜態變數key_up預設為1,表示按鍵處於空閒狀態(鬆開)。
如果選擇短按,在按鍵處於空閒狀態時,檢測到KEY0WK_UP中任意一個按鍵被按下,則將key_up置0,在此期間不處理其他按鍵判斷,函數返回值為按鍵值或0(無按鍵);當按鍵鬆開,程式再次執行到按鍵掃描函數中時,key_up置為1,按鍵再次回到空閒狀態。
如果選擇長按,則key_up恆為1,無論是否有按鍵正處於按下狀態,每次進入KEY_Scan函數都進行按鍵判斷,這樣就實現了按鍵的長按檢測。

u8 KEY_Scan(u8 mode)
{	 
	static u8 key_up=1;//按鍵按鬆開標誌
	if(mode)key_up=1;  //支援連按		  
	if(key_up&&(KEY0==1||WK_UP==1))
	{
		delay_ms(10);//去抖動 
		key_up=0;
		if(KEY0==1)return KEY0_PRES;
		else if(WK_UP==1)return WKUP_PRES;
	}else if(KEY0==0&&WK_UP==0)key_up=1; 	    
 	return 0;// 無按鍵按下
}

實驗3 串列埠實驗

mian()

與前兩個實驗相比,串列埠實驗增加了NVIC中斷設定、串列埠初始化設定。main函數實現的功能為:微控制器不停地向串列埠傳送提示性資料,如果有外部裝置通過串列埠向微控制器傳送資料(以「\r\n「作為結束符),微控制器接收資料並返回給外部裝置。


 int main(void)
 {		
 	u16 t;  
	u16 len;	
	u16 times=0;
	delay_init();	    	 //延時函數初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //設定NVIC中斷分組2:2位搶佔優先順序,2位響應優先順序
	uart_init(115200);	 //串列埠初始化為115200
 	LED_Init();			     //LED埠初始化
	KEY_Init();          //初始化與按鍵連線的硬體介面
 	while(1)
	{
		if(USART_RX_STA&0x8000)
		{					   
			len=USART_RX_STA&0x3fff;//得到此次接收到的資料長度
			printf("\r\n您傳送的訊息為:\r\n\r\n");
			for(t=0;t<len;t++)
			{
				USART_SendData(USART1, USART_RX_BUF[t]);//向串列埠1傳送資料
				while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待傳送結束
			}
			printf("\r\n\r\n");//插入換行
			USART_RX_STA=0;
		}else
		{
			times++;
			if(times%5000==0)
			{
				printf("\r\n戰艦STM32開發板 串列埠實驗\r\n");
				printf("正點原子@ALIENTEK\r\n\r\n");
			}
			if(times%200==0)printf("請輸入資料,以確認鍵結束\n");  
			if(times%30==0)LED0=!LED0;//閃爍LED,提示系統正在執行.
			delay_ms(10);   
		}
	}	 
 }

NVIC_PriorityGroupConfig

NVIC:巢狀向量中斷控制器,NVIC_PriorityGroupConfig函數是中斷優先順序的分組設定函數。


/**
  * @brief  Configures the priority grouping: pre-emption priority and subpriority.
  * @param  NVIC_PriorityGroup: specifies the priority grouping bits length. 
  *   This parameter can be one of the following values:
  *     @arg NVIC_PriorityGroup_0: 0 bits for pre-emption priority
  *                                4 bits for subpriority
  *     @arg NVIC_PriorityGroup_1: 1 bits for pre-emption priority
  *                                3 bits for subpriority
  *     @arg NVIC_PriorityGroup_2: 2 bits for pre-emption priority
  *                                2 bits for subpriority
  *     @arg NVIC_PriorityGroup_3: 3 bits for pre-emption priority
  *                                1 bits for subpriority
  *     @arg NVIC_PriorityGroup_4: 4 bits for pre-emption priority
  *                                0 bits for subpriority
  * @retval None
  */
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
{
  /* Check the parameters */
  assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup));
  
  /* Set the PRIGROUP[10:8] bits according to NVIC_PriorityGroup value */
  SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
}

中斷優先順序分為搶佔式優先順序響應優先順序,搶佔優先順序越高的先處理,當兩個中斷向量的搶佔優先順序相同時,如果兩個中斷同時到達, 則先處理響應優先順序高的中斷。

如果對兩種優先順序的位數分配進行分組,可以分為5組(0~4),分組設定是在暫存器SCB->AIRCR中設定:
在這裡插入圖片描述
main函數中,分組為:

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //設定NVIC中斷分組2:2位搶佔優先順序,2位響應優先順序

注:這個分組只是設定STM32中斷的兩種優先順序可選範圍,比如0組中,沒有搶佔優先順序,一般情況(學習過程中)該設定設定為2組就行了。另外,這個分組是全域性的,所以一個程式中只需要設定一次,多次設定可能會導致未知錯誤。

uart_init(…)函數

串列埠初始化函數裡不僅有GPIO初始化,還有UART初始化和NVIC初始化。

void uart_init(u32 bound){
  //GPIO埠設定
  GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	 
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA時鐘
  
	//USART1_TX   GPIOA.9
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//複用推輓輸出
  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
   
  //USART1_RX	  GPIOA.10初始化
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空輸入
  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10  

  //Usart1 NVIC 設定
  NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//搶佔優先順序3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子優先順序3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//根據指定的引數初始化VIC暫存器
  
   //USART 初始化設定

	USART_InitStructure.USART_BaudRate = bound;//串列埠波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字長為8位元資料格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//一個停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//無奇偶校驗位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//無硬體資料流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收發模式

  USART_Init(USART1, &USART_InitStructure); //初始化串列埠1
  USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//開啟串列埠接受中斷
  USART_Cmd(USART1, ENABLE);                    //使能串列埠1 

}

從原理圖可知串列埠的傳送IO為GPIOA9,接收IO為GPIOA10,TX(PA9)設定為複用推輓輸出(PA9為複用引腳,可以通過設定複用推輓輸出完成USART_TX功能的設定,另外還可以通過配合複用暫存器方式實現複用,如PWM實驗),RX設定為浮空輸入。
在這裡插入圖片描述

串列埠設定一般可以分為以下幾步:

  1. 定義USART_InitTypeDef結構體成員
  2. 使能USART外設時鐘RCC_APB2PeriphClockCmd(...),根據手冊檢視對應時鐘匯流排(LED_init中有介紹)
  3. 設定波特率USART_BaudRate,可以通過外部傳參設定
  4. 設定字長USART_WordLength,一般為8位元組
  5. 設定停止位USART_StopBits,一般為1個停止位
  6. 設定校驗方式USART_Parity
  7. 設定流控制USART_HardwareFlowControl,一般無控制
  8. 設定收發模式USART_Mode,設定為收發共用
  9. 初始化USART_InitTypeDef成員USART_Init(...)
  10. 如果要使用串列埠中斷,還需要設定串列埠中斷USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE)
  11. 使能串列埠USART_Cmd(USARTx, ENABLE)

NVIC中斷設定一般步驟:

  1. 定義一個NVIC_InitTypeDef結構體成員
  2. 設定中斷通道NVIC_IRQChannel
  3. 設定搶佔優先順序NVIC_IRQChannelPreemptionPriority,大小範圍由分組決定
  4. 設定響應優先順序NVIC_IRQChannelSubPriority
  5. 使能中斷NVIC_IRQChannelCmd,ENABLE為使能
  6. 初始化NVIC_InitTypeDef成員,NVIC_Init(...)

串列埠處理全域性變數

//串列埠1中斷服務程式
//注意,讀取USARTx->SR能避免莫名其妙的錯誤   	
u8 USART_RX_BUF[USART_REC_LEN];     //接收緩衝,最大USART_REC_LEN個位元組.
//接收狀態
//bit15,	接收完成標誌
//bit14,	接收到0x0d
//bit13~0,	接收到的有效位元組數目
u16 USART_RX_STA=0;       //接收狀態標記

USART_RX_BUF為程式定義的全域性接收緩衝, USART_REC_LEN為緩衝最大位元組數。
USART_RX_STA為程式定義的全域性狀態值,表述串列埠1的接收狀態,16位元,0-13位為接收到的位元組數,14位元為1表示接收到0x0d(’\r’),15位資料為1表示接收完成。

重寫printf(fputc)

USART_SendData(...)是串列埠傳送位元組資料的庫函數,如果我們想傳送字串,用這個函數就不太方便,如果我們重寫fputc函數,就能直接用printf來列印字串到串列埠,方法如下。
USART_GetFlagStatus函數的作用是獲取串列埠狀態,USART_FLAG_TC參數列示接收完成狀態。
【注意】使用該方式重寫fputc,一定要開啟Keil的Use MicroLIB,否則程式可能無法執行。

/*使用microLib的方法*/
 
int fputc(int ch, FILE *f)
{
	USART_SendData(USART1, (uint8_t) ch);

	while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) {}	
   
    return ch;
}

USART1_IRQHandler串列埠中斷函數

STM32的中斷服務函數名稱是固定,是通過函數名與底層繫結。
在這裡插入圖片描述

void USART1_IRQHandler(void)                	//串列埠1中斷服務程式
	{
	u8 Res;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中斷(接收到的資料必須是0x0d 0x0a結尾)
		{
		Res =USART_ReceiveData(USART1);	//讀取接收到的資料
		
		if((USART_RX_STA&0x8000)==0)//接收未完成
			{
				if(USART_RX_STA&0x4000)//接收到了0x0d
				{
					if(Res!=0x0a)USART_RX_STA=0;//接收錯誤,重新開始
					else USART_RX_STA|=0x8000;	//接收完成了 
				}
				else //還沒收到0X0D
				{	
					if(Res==0x0d)USART_RX_STA|=0x4000;
					else
					{
						USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
						USART_RX_STA++;
						if(USART_RX_STA>(USART_REC_LEN-1))
							USART_RX_STA=0;//接收資料錯誤,重新開始接收	  
					}		 
				}
			}   		 
     } 
} 

當串列埠接收到資料,中斷服務函數自動執行,先使用USART_GetITStatus(...)判斷USART1的中斷接受狀態USART_IT_RXNE,如果接收完成,使用USART_ReceiveData(...)接收串列埠資料,串列埠資料接收以1個位元組為單位,當接收到回車時(’\r‘和\‘n’),完成接收,此時主函數開始處理接收到的資料。

實驗結果
在這裡插入圖片描述


這。。。。。。😵,才完成3個實驗啊。

在這裡插入圖片描述


實驗4 外部中斷實驗

STM32的中斷控制器支援19個外部中斷/事件請求:
線0~15:對應外部IO口的輸入中斷。
線16:連線到PVD輸出。
線17:連線到RTC鬧鐘事件。
線18:連線到USB喚醒事件。
下圖為IO口對應的中斷線:
在這裡插入圖片描述

main()

該實驗僅僅比實驗3多了一個EXTIX_Init(),主函數沒有什麼程式碼,這就說明功能全搬到了外部中斷服務函數中。

 int main(void)
 {		
 
	delay_init();	    	 //延時函數初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //設定NVIC中斷分組2:2位搶佔優先順序,2位響應優先順序
	uart_init(115200);	 //串列埠初始化為115200
 	LED_Init();		  		//初始化與LED連線的硬體介面
	BEEP_Init();         	//初始化蜂鳴器埠
	KEY_Init();         	//初始化與按鍵連線的硬體介面
	EXTIX_Init();		 	//外部中斷初始化
	LED0=0;					//點亮LED0
	while(1)
	{	    
		printf("OK\r\n");	
		delay_ms(1000);	  
	}
 }

EXTIX_Init()函數

初始化函數中,設定了兩個外部中斷(EXTI0和EXIT4),對應開發板上的兩個按鍵。

//外部中斷0服務程式
void EXTIX_Init(void)
{
 
 	EXTI_InitTypeDef EXTI_InitStructure;
 	NVIC_InitTypeDef NVIC_InitStructure;

    KEY_Init();	 //	按鍵埠初始化

  	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);	//使能複用功能時鐘

   //GPIOE.4	  中斷線以及中斷初始化設定  上升沿觸發	//KEY0
  	GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4);
	EXTI_InitStructure.EXTI_Line=EXTI_Line4;	//KEY0
  	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;	
  	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
  	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
  	EXTI_Init(&EXTI_InitStructure);	 	//根據EXTI_InitStruct中指定的引數初始化外設EXTI暫存器

	//GPIOA.0	  中斷線以及中斷初始化設定 上升沿觸發 PA0  WK_UP
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0); 
  	EXTI_InitStructure.EXTI_Line=EXTI_Line0;
  	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
  	EXTI_Init(&EXTI_InitStructure);		//根據EXTI_InitStruct中指定的引數初始化外設EXTI暫存器


  	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;			//使能按鍵WK_UP所在的外部中斷通道
  	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;	//搶佔優先順序2, 
  	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03;					//子優先順序3
  	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;								//使能外部中斷通道
  	NVIC_Init(&NVIC_InitStructure); 

	NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;			//使能按鍵KEY0所在的外部中斷通道
  	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;	//搶佔優先順序2 
  	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;					//子優先順序0 
  	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;								//使能外部中斷通道
  	NVIC_Init(&NVIC_InitStructure);  	  //根據NVIC_InitStruct中指定的引數初始化外設NVIC暫存器
 
}

外部中斷初始化設定一般步驟:

  1. 定義一個EXTI_InitTypeDef結構體成員
  2. 外部中斷(複用IO,AFIO)時鐘使能,RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
  3. IO中斷線設定,GPIO_EXTILineConfig(...)
  4. 設定中斷線EXTI_Line
  5. 設定中斷模式EXTI_Mode
  6. 設定觸發方式EXTI_Trigger
  7. 使能中斷線EXTI_LineCmd
  8. 初始化EXTI_InitTypeDef成員,

EXTIx_IRQHandler外部中斷函數

當按鍵按下,IO檢測到上升沿,觸發中斷,執行對應中斷服務函數,函數中完成了LED1LED0的亮滅控制,每次進入中斷後,必須手動清除中斷標誌EXTI_ClearITPendingBit(...),不然中斷函數會一直執行。

//外部中斷0服務程式 
void EXTI0_IRQHandler(void)
{
	delay_ms(10);//消抖
	if(WK_UP==1)	 	 //WK_UP按鍵
	{				 
		LED1=!LED1;	
	}
	EXTI_ClearITPendingBit(EXTI_Line0); //清除LINE0上的中斷標誌位  
}
 
//外部中斷4服務程式
void EXTI4_IRQHandler(void)
{
	delay_ms(10);//消抖
	if(KEY0==1)	 //按鍵KEY0
	{
		LED0=!LED0;
	}		 
	EXTI_ClearITPendingBit(EXTI_Line4);  //清除LINE4上的中斷標誌位  
}

實驗5 獨立看門狗實驗

STM32看門狗
在由微控制器構成的微型計算機系統中,由於微控制器的工作常常會受到來自外界電磁場的干擾,造成程式的跑飛,而陷入死迴圈,程式的正常執行被打斷,由微控制器控制的系統無法繼續工作,會造成整個系統的陷入停滯狀態,發生不可預料的後果,所以出於對微控制器執行狀態進行實時監測的考慮,便產生了一種專門用於監測微控制器程式執行狀態的模組或者晶片,俗稱「看門狗」(watchdog) 。
STM32內建兩個看門狗,提供了更高的安全性,時間的精確性和使用的靈活性。兩個看門狗裝置(獨立看門狗/視窗看門狗)可以用來檢測和解決由軟體錯誤引起的故障。當計數器達到給定的超時值時,觸發一箇中斷(僅適用視窗看門狗)或者產生系統復位。
獨立看門狗(IWDG)由專用的低速時鐘(LSI)驅動,即使主時鐘發生故障它仍有效。
獨立看門狗適合應用於需要看門狗作為一個在主程式之外 能夠完全獨立工作,並且對時間精度要求低的場合。
視窗看門狗由從APB1時鐘分頻後得到時鐘驅動。通過可設定的時間視窗來檢測應用程式非正常的過遲或過早操作。
視窗看門狗最適合那些要求看門狗在精確計時視窗起作用的程式。
——正點原子課程PPT

IWDG(Independent watchdog)獨立看門狗,可以用來檢測並解決由於軟體錯誤導致的故障,當計數器到達給定的超時值時,會觸發一箇中斷或產生系統復位。

main()

新增兩個函數:IWDG_Init(...)IWDG_Feed(),本實驗通過按鍵WKUP喂狗,如果超過1s沒有喂狗,微控制器就復位(LED一直亮說明沒復位)。

 int main(void)
 {		
	delay_init();	    	 //延時函數初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 	 //設定NVIC中斷分組2:2位搶佔優先順序,2位響應優先順序
	uart_init(115200);	 //串列埠初始化為115200
 	LED_Init();		  	 //初始化與LED連線的硬體介面
	KEY_Init();          //按鍵初始化	 
	delay_ms(500);   	 //讓人看得到滅
	IWDG_Init(4,625);    //與分頻數為64,過載值為625,溢位時間為1s	   
	LED0=0;				 //點亮LED0
	while(1)
	{
		if(KEY_Scan(0)==WKUP_PRES)
		{
			IWDG_Feed();//如果WK_UP按下,則喂狗
		}
		delay_ms(10);
	};	 
}

IWDG_Init(…)函數

初始化獨立看門狗,需要先用IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);使能相關暫存器的寫許可權,然後用IWDG_SetPrescaler(prer)IWDG_SetReload(rlr)設定預分頻值prer和過載值rlr,接著用IWDG_ReloadCounter()重灌載計數器(喂狗),最後使能看門狗——IWDG_Enable()

//初始化獨立看門狗
//prer:分頻數:0~7(只有低3位有效!)
//分頻因子=4*2^prer.但最大值只能是256!
//rlr:重灌載暫存器值:低11位有效.
//時間計算(大概):Tout=((4*2^prer)*rlr)/40 (ms).
void IWDG_Init(u8 prer,u16 rlr) 
{	
 	IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);  //使能對暫存器IWDG_PR和IWDG_RLR的寫操作
	
	IWDG_SetPrescaler(prer);  //設定IWDG預分頻值:設定IWDG預分頻值為64
	
	IWDG_SetReload(rlr);  //設定IWDG重灌載值
	
	IWDG_ReloadCounter();  //按照IWDG重灌載暫存器的值重灌載IWDG計數器
	
	IWDG_Enable();  //使能IWDG
}

從庫函數裡的宏定義我們可以瞭解到看門狗在喂狗使能時向IWDG_KR傳送的資料。

/* KR register bit mask */
#define KR_KEY_Reload    ((uint16_t)0xAAAA)
#define KR_KEY_Enable    ((uint16_t)0xCCCC)

IWDG_Feed()函數

過載計數器即為喂狗

//喂獨立看門狗
void IWDG_Feed(void)
{   
 	IWDG_ReloadCounter();//reload										   
}

實驗6 視窗看門狗實驗[待學習]

視窗看門狗(Window watchdog)通常被用來監測由外部干擾或不可預見的邏輯條件造成的應用程式背離正常的執行序列而產生的軟體故障。
之所以稱為視窗就是因為其喂狗時間是一個有上下限的範圍(視窗),你可以通過設定相關暫存器,設定其上限時間(下限固定)。喂狗的時間不能過早也不能過晚。

為什麼要視窗看門狗?
對於一般的看門狗,程式可以在它產生復位前的任意時刻重新整理看門狗,但這有一個隱患,有可能程式跑亂了又跑回到正常的地方,或跑亂的程式正好執行了重新整理看門狗操作,這樣的情況下一般的看門狗就檢測不出來了;
如果使用視窗看門狗,程式設計師可以根據程式正常執行的時間設定重新整理看門狗的一個時間視窗,保證不會提前重新整理看門狗也不會滯後重新整理看門狗,這樣可以檢測出程式沒有按照正常的路徑執行非正常地跳過了某些程式段的情況。
——正點原子課程PPT

視窗看門狗以前沒認真學,現在一點概念都沒有了,先記錄下,以後再完善吧。。。

main()

 int main(void)
 {		
	delay_init();	    	 //延時函數初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//設定中斷優先順序分組為組2:2位搶佔優先順序,2位響應優先順序
	uart_init(115200);	 //串列埠初始化為115200
 	LED_Init();
	KEY_Init();          //按鍵初始化	 
	LED0=0;
	delay_ms(300);	  
	WWDG_Init(0X7F,0X5F,WWDG_Prescaler_8);//計數器值為7f,視窗暫存器為5f,分頻數為8	   
 	while(1)
	{
		LED0=1;			  	   
	}   
}
  

WWDG_Init(…)函數

//儲存WWDG計數器的設定值,預設為最大. 
u8 WWDG_CNT=0x7f; 
//初始化視窗看門狗 	
//tr   :T[6:0],計數器值 
//wr   :W[6:0],視窗值 
//fprer:分頻係數(WDGTB),僅最低2位有效 
//Fwwdg=PCLK1/(4096*2^fprer). 

void WWDG_Init(u8 tr,u8 wr,u32 fprer)
{ 
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);  //   WWDG時鐘使能

	WWDG_CNT=tr&WWDG_CNT;   //初始化WWDG_CNT.   
	WWDG_SetPrescaler(fprer);設定IWDG預分頻值

	WWDG_SetWindowValue(wr);//設定視窗值

	WWDG_Enable(WWDG_CNT);	 //使能看門狗 ,	設定 counter .                  

	WWDG_ClearFlag();//清除提前喚醒中斷標誌位 

	WWDG_NVIC_Init();//初始化視窗看門狗 NVIC

	WWDG_EnableIT(); //開啟視窗看門狗中斷
} 

//視窗看門狗中斷服務程式
void WWDG_NVIC_Init()
{
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = WWDG_IRQn;    //WWDG中斷
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;   //搶佔2,子優先順序3,組2	
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;	 //搶佔2,子優先順序3,組2	
  	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; 
	NVIC_Init(&NVIC_InitStructure);//NVIC初始化
}

void WWDG_IRQHandler(void)
{

	WWDG_SetCounter(WWDG_CNT);	  //當禁掉此句後,視窗看門狗將產生復位

	WWDG_ClearFlag();	  //清除提前喚醒中斷標誌位

	LED1=!LED1;		 //LED狀態翻轉
}

實驗7 定時器中斷實驗

main()

主函數僅僅多了定時器初始化函數TIM3_Int_Init(...)

 int main(void)
 {		
 
	delay_init();	    	 //延時函數初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //設定NVIC中斷分組2:2位搶佔優先順序,2位響應優先順序
	uart_init(115200);	 //串列埠初始化為115200
 	LED_Init();			     //LED埠初始化
	TIM3_Int_Init(4999,7199);//10Khz的計數頻率,計數到5000為500ms  
   	while(1)
	{
		LED0=!LED0;
		delay_ms(200);		   
	}	 
}	

TIM3_Int_Init(…)函數

在這裡插入圖片描述
定時器3掛載在APB1時鐘匯流排上。

//通用定時器3中斷初始化
//這裡時鐘選擇為APB1的2倍,而APB1為36M
//arr:自動重灌值。
//psc:時鐘預分頻數
//這裡使用的是定時器3!
void TIM3_Int_Init(u16 arr,u16 psc)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //時鐘使能
	
	//定時器TIM3初始化
	TIM_TimeBaseStructure.TIM_Period = arr; //設定在下一個更新事件裝入活動的自動重灌載暫存器週期的值	
	TIM_TimeBaseStructure.TIM_Prescaler =psc; //設定用來作為TIMx時脈頻率除數的預分頻值
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //設定時鐘分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上計數模式
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根據指定的引數初始化TIMx的時間基數單位
 
	TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //使能指定的TIM3中斷,允許更新中斷

	//中斷優先順序NVIC設定
	NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;  //TIM3中斷
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  //先佔優先順序0級
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;  //從優先順序3級
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
	NVIC_Init(&NVIC_InitStructure);  //初始化NVIC暫存器


	TIM_Cmd(TIM3, ENABLE);  //使能TIMx					 
}

基本定時器初始化設定一般步驟

  1. 定義一個TIM_TimeBaseInitTypeDef結構體成員
  2. 使能外設時鐘,TIM3:RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE)
  3. 設定週期TIM_Period
  4. 設定預分頻係數TIM_Prescaler
  5. 設定時鐘分割TIM_ClockDivision
  6. 設定計數方向TIM_CounterMode
  7. 初始化TIM_TimeBaseInitTypeDef成員,TIM_TimeBaseInit(...)
  8. 如果開啟中斷,則需要使能對應中斷TIM_ITConfig(TIMx,TIM_IT_Update,ENABLE );
  9. 使能定時器TIM_Cmd(TIMx, ENABLE)

在這裡插入圖片描述
TIM3在APB1時鐘匯流排上,APB1的時鐘為36MHz,但TIM3的時鐘是APB1的兩倍,即72MHz。

定時器設定完後,下一步就是了解如何設定計時的引數,main函數中定時器初始化:TIM3_Int_Init(4999,7199),TIM3的時脈頻率為72MHz,預分頻係數為7200 - 1(定時器計時溢位後的再計一次數才觸發中斷,所以要減1),72000000 / 7200 = 10000(Hz),1 / 10000 = 0.1ms,計數值從0開始,所以4999表示計時5000次,5000 * 0.1ms = 500ms。

TIMx_IRQHandler()定時器中斷函數

當定時器中斷更新,微控制器執行定時器的中斷服務函數,記得在函數中用TIM_ClearITPendingBit(...)來清除對應的中斷位。

//定時器3中斷服務程式
void TIM3_IRQHandler(void)   //TIM3中斷
{
	if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //檢查TIM3更新中斷髮生與否
		{
		TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  //清除TIMx更新中斷標誌 
		LED1=!LED1;
		}
}

實驗8 PWM輸出實驗

PWM產生原理:在定時器一個計時週期內,計數值低於比較值時,IO輸出低電平(或高電平),當計數值高於比較值,IO輸出相反的電平,這樣就產生了所謂的PWM波,比較值CCR決定PWM的佔空比,過載值ARR決定PWM的週期(頻率)。
在這裡插入圖片描述

main()

這個實驗一看便知道是呼吸燈了,直接分析PWM設定吧。
main函數裡用到了TIM_SetCompare2(...)函數,它的作用是設定定時器比較值,即PWM佔空比

 int main(void)
 {		
 	u16 led0pwmval=0;
	u8 dir=1;	
	delay_init();	    	 //延時函數初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 	 //設定NVIC中斷分組2:2位搶佔優先順序,2位響應優先順序
	uart_init(115200);	 //串列埠初始化為115200
 	LED_Init();			     //LED埠初始化
 	TIM3_PWM_Init(899,0);	 //不分頻。PWM頻率=72000000/900=80Khz
   	while(1)
	{
 		delay_ms(10);	 
		if(dir)led0pwmval++;
		else led0pwmval--;

 		if(led0pwmval>300)dir=0;
		if(led0pwmval==0)dir=1;										 
		TIM_SetCompare2(TIM3,led0pwmval);		   
	}	 
 }

TIM3_PWM_Init(…)函數


//TIM3 PWM部分初始化 
//PWM輸出初始化
//arr:自動重灌值
//psc:時鐘預分頻數
void TIM3_PWM_Init(u16 arr,u16 psc)
{  
	GPIO_InitTypeDef GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	TIM_OCInitTypeDef  TIM_OCInitStructure;
	

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);	//使能定時器3時鐘
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB  | RCC_APB2Periph_AFIO, ENABLE);  //使能GPIO外設和AFIO複用功能模組時鐘
	
	GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); //Timer3部分重對映  TIM3_CH2->PB5    
 
   //設定該引腳為複用輸出功能,輸出TIM3 CH2的PWM脈衝波形	GPIOB.5
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //TIM_CH2
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //複用推輓輸出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIO
 
   //初始化TIM3
	TIM_TimeBaseStructure.TIM_Period = arr; //設定在下一個更新事件裝入活動的自動重灌載暫存器週期的值
	TIM_TimeBaseStructure.TIM_Prescaler =psc; //設定用來作為TIMx時脈頻率除數的預分頻值 
	TIM_TimeBaseStructure.TIM_ClockDivision = 0; //設定時鐘分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上計數模式
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根據TIM_TimeBaseInitStruct中指定的引數初始化TIMx的時間基數單位
	
	//初始化TIM3 Channel2 PWM模式	 
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //選擇定時器模式:TIM脈衝寬度調變模式2
 	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比較輸出使能
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //輸出極性:TIM輸出比較極性高
	TIM_OC2Init(TIM3, &TIM_OCInitStructure);  //根據T指定的引數初始化外設TIM3 OC2

	TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);  //使能TIM3在CCR2上的預裝載暫存器
 
	TIM_Cmd(TIM3, ENABLE);  //使能TIM3
}

在這裡插入圖片描述
開發板上PB5對應LED1,如果要將它作為TIM3_CH2來輸出PWM波,必須先設定複用暫存器(選擇部分重映像)。步驟:

  1. 使能複用時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE)
  2. 使能TIM3部分重映像GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE)

除此以外,PB5也需要重新設定,模式選擇複用推輓輸出

定時器PWM設定一般步驟

  1. 定義一個TIM_OCInitTypeDef結構體成員
  2. 設定比較輸出的模式TIM_OCModeTIM_OCMode_PWM2
  3. 設定輸出狀態TIM_OutputStateTIM_OutputState_Enable
  4. 設定輸出極性TIM_OCPolarity
  5. 初始化TIM_OCInitTypeDef成員,TIM_OC2Init(...)
  6. 使能定時器輸出比較預裝載暫存器,TIM_OC2PreloadConfig(TIMx, TIM_OCPreload_Enable)

此時,我內心已經絕望,原來我這麼菜🥱,後面幾個實驗學起來都感覺很生疏了,當初偷的懶,現在得來償還。
在這裡插入圖片描述


實驗9 輸入捕獲實驗[待學習]

說實話,我都忘了什麼叫輸入捕獲。

一句話總結工作過程:通過檢測TIMx_CHx上的邊沿訊號,在邊沿訊號發生跳變(比如上升沿/下降沿)的時候,將當前定時器的值(TIMx_CNT)存放到對應的捕獲/比較暫存器(TIMx_CCRx)裡面,完成一次捕獲。
——正點原子課程PPT

main()

main函數分析了半天,開始以為本實驗是用TIM5捕獲TIM3的輸出 ,後來發現PB5和PA0根本沒有引出IO引腳(PB5為LED1,PA0為KEY_UP),所以本實驗的功能為檢測按鍵按下的時間並列印到串列埠。這麼說輸入捕獲和邏輯分析儀有點類似。

extern u8  TIM5CH1_CAPTURE_STA;		//輸入捕獲狀態		    				
extern u16	TIM5CH1_CAPTURE_VAL;	//輸入捕獲值	
 int main(void)
 {		
 	u32 temp=0; 
	delay_init();	    	 //延時函數初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);	 //設定NVIC中斷分組2:2位搶佔優先順序,2位響應優先順序
	uart_init(115200);	 //串列埠初始化為115200
 	LED_Init();			     //LED埠初始化
 
 	TIM3_PWM_Init(899,0); 		//不分頻。PWM頻率=72000/(899+1)=80Khz
 	TIM5_Cap_Init(0XFFFF,72-1);	//以1Mhz的頻率計數 
   	while(1)
	{
 		delay_ms(10);
		TIM_SetCompare2(TIM3,TIM_GetCapture2(TIM3)+1);

		if(TIM_GetCapture2(TIM3)==300)TIM_SetCompare2(TIM3,0);	
		 		 
 		if(TIM5CH1_CAPTURE_STA&0X80)//成功捕獲到了一次上升沿
		{
			temp=TIM5CH1_CAPTURE_STA&0X3F;
			temp*=65536;//溢位時間總和
			temp+=TIM5CH1_CAPTURE_VAL;//得到總的高電平時間
			printf("HIGH:%d us\r\n",temp);//列印總的高點平時間
			TIM5CH1_CAPTURE_STA=0;//開啟下一次捕獲
		}
	}
 }

TIM5_Cap_Init(…)函數

//定時器5通道1輸入捕獲設定

TIM_ICInitTypeDef  TIM5_ICInitStructure;

void TIM5_Cap_Init(u16 arr,u16 psc)
{	 
	GPIO_InitTypeDef GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
   	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE);	//使能TIM5時鐘
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  //使能GPIOA時鐘
	
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;  //PA0 清除之前設定  
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0 輸入  
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	GPIO_ResetBits(GPIOA,GPIO_Pin_0);						 //PA0 下拉
	
	//初始化定時器5 TIM5	 
	TIM_TimeBaseStructure.TIM_Period = arr; //設定計數器自動重灌值 
	TIM_TimeBaseStructure.TIM_Prescaler =psc; 	//預分頻器   
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //設定時鐘分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上計數模式
	TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根據TIM_TimeBaseInitStruct中指定的引數初始化TIMx的時間基數單位
  
	//初始化TIM5輸入捕獲引數
	TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //CC1S=01 	選擇輸入端 IC1對映到TI1上
  	TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;	//上升沿捕獲
  	TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //對映到TI1上
  	TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;	 //設定輸入分頻,不分頻 
  	TIM5_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 設定輸入濾波器 不濾波
  	TIM_ICInit(TIM5, &TIM5_ICInitStructure);
	
	//中斷分組初始化
	NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn;  //TIM3中斷
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;  //先佔優先順序2級
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  //從優先順序0級
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
	NVIC_Init(&NVIC_InitStructure);  //根據NVIC_InitStruct中指定的引數初始化外設NVIC暫存器 
	
	TIM_ITConfig(TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允許更新中斷 ,允許CC1IE捕獲中斷	
	
   	TIM_Cmd(TIM5,ENABLE ); 	//使能定時器5
   
}

定時器輸入捕獲設定流程一般為

  1. 定義一個TIM_ICInitTypeDef結構體成員
  2. 設定通道TIM_Channel
  3. 設定輸入捕獲電平TIM_ICPolarity
  4. 設定輸入捕獲方向TIM_ICSelection
  5. 設定輸入捕獲預分頻係數TIM_ICPrescaler
  6. 設定輸入捕獲濾波器TIM_ICFilter
  7. 初始化TIM_ICInitTypeDef成員,TIM_ICInit(...)
  8. 如果開啟捕獲中斷:TIM_ITConfig(TIMx, TIM_TI_CC1, ENABLE)

TIM5_IRQHandler() 處理輸入捕獲


u8  TIM5CH1_CAPTURE_STA=0;	//輸入捕獲狀態		    				
u16	TIM5CH1_CAPTURE_VAL;	//輸入捕獲值
 
//定時器5中斷服務程式	 
void TIM5_IRQHandler(void)
{ 

 	if((TIM5CH1_CAPTURE_STA&0X80)==0)//還未成功捕獲	
	{	  
		if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)
		 
		{	    
			if(TIM5CH1_CAPTURE_STA&0X40)//已經捕獲到高電平了
			{
				if((TIM5CH1_CAPTURE_STA&0X3F)==0X3F)//高電平太長了
				{
					TIM5CH1_CAPTURE_STA|=0X80;//標記成功捕獲了一次
					TIM5CH1_CAPTURE_VAL=0XFFFF;
				}else TIM5CH1_CAPTURE_STA++;
			}	 
		}
	if (TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)//捕獲1發生捕獲事件
		{	
			if(TIM5CH1_CAPTURE_STA&0X40)		//捕獲到一個下降沿 		
			{	  			
				TIM5CH1_CAPTURE_STA|=0X80;		//標記成功捕獲到一次高電平脈寬
				TIM5CH1_CAPTURE_VAL=TIM_GetCapture1(TIM5);
		   		TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Rising); //CC1P=0 設定為上升沿捕獲
			}else  								//還未開始,第一次捕獲上升沿
			{
				TIM5CH1_CAPTURE_STA=0;			//清空
				TIM5CH1_CAPTURE_VAL=0;
	 			TIM_SetCounter(TIM5,0);
				TIM5CH1_CAPTURE_STA|=0X40;		//標記捕獲到了上升沿
		   		TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling);		//CC1P=1 設定為下降沿捕獲
			}		    
		}			     	    					   
 	}
 
    TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update); //清除中斷標誌位
 
}

實驗10 TFTLCD顯示實驗[放棄]

LCD,額🥱,正點原子的LCD程式碼相容了好幾款螢幕,驅動程式碼3000+行,這個實驗就算了。

今天的學習就到此為止吧,路還很長!

已經是另一天了,從13:56到0:21,差不多10個多小時,任務失敗了,mission failed!
在這裡插入圖片描述