STM32 時延函數之阻塞和非阻塞的實現討論

2020-08-11 20:36:10

一 、常使用的幾種延時方式

1 自帶的hal_delay 函數 毫秒級延遲

void HAL_Delay(__IO uint32_t Delay)
{
  uint32_t tickstart = HAL_GetTick(); //獲取tick值(毫秒)
  uint32_t wait = Delay;
 
  /* Add a period to guarantee minimum wait */
  if (wait < HAL_MAX_DELAY)
  {
     wait++;//傳參,延時的時間
  }
 
  while((HAL_GetTick() - tickstart) < wait)
  {
  }
}
也可以設定爲us延時,改變函數參數

設定方法:也可以設定爲1us

// HAL_RCC_GetHCLKFreq()/1000 1ms中斷一次,即HAL_Delay函數延時基準爲1ms
// HAL_RCC_GetHCLKFreq()/100000  10us中斷一次,即HAL_Delay函數延時基準爲10us
// HAL_RCC_GetHCLKFreq()/1000000 1us中斷一次,即HAL_Delay函數延時基準爲1us
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000000);  // 設定並啓動系統滴答定時器

2 中斷延時----利用定時器計時

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  LED_GPIO_Init();
  /* 基本定時器初始化:1ms中斷一次 */
  BASIC_TIMx_Init();
  /* 在中斷模式下啓動定時器 */
  HAL_TIM_Base_Start_IT(&htimx);
 
  while (1)
  {
    if(timer_count==1000)
    {
      timer_count=0;
      LED1_TOGGLE;
    }
  }
}
 
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  timer_count++;  //回撥函數
}

3 優選方式------獲取系統時鐘計時,非阻塞式延時

 void delay_ms(int32_t nms) 
 {
  int32_t temp; 
  SysTick->LOAD = 8000*nms; 
  SysTick->VAL=0X00;//清空計數器 
  SysTick->CTRL=0X01;//使能,減到零是無動作,採用外部時鐘源 
  do 
  { 
       temp=SysTick->CTRL;//讀取當前倒計數值 
  }
     while((temp&0x01)&&(!(temp&(1<<16))));//等待時間到達 
     
     SysTick->CTRL=0x00; //關閉計數器 
     SysTick->VAL =0X00; //清空計數器 
 } 

二 、存在的問題

1. HAL庫延時函數HAL_Delay()遇坑,

初學HAL庫感受着它強大的封裝庫可以拿來直接用,對於初學者很容易上手,看來以後小學生都能寫微控制器了
問題:延時函數一直用的HAL_Delay()毫秒級延時,但是在中斷中呼叫延時函數卻卡死在這裏,從封裝函數中可以發現函數中有中斷獲取系統時鐘HAL_IncTick(void),由於優先順序系統給的低,所以在高優先順序的中斷中無法產生這個低階的中斷,導致程式卡死在HAL_Delay()中。

void delay_ms(int32_t nms) 

 {

  int32_t temp; 

  SysTick->LOAD = 8000*nms; 

  SysTick->VAL=0X00;//清空計數器 

  SysTick->CTRL=0X01;//使能,減到零是無動作,採用外部時鐘源 

  do 

  { 

       temp=SysTick->CTRL;//讀取當前倒計數值 

  }

     while((temp&0x01)&&(!(temp&(1<<16))));//等待時間到達 

     

     SysTick->CTRL=0x00; //關閉計數器 

     SysTick->VAL =0X00; //清空計數器 

 }

注意:該函數好像和HAL_Delay()有衝突?不能混用在一起

2. HAL_Delay()函數鎖死

HAL_Delay函數用的是中斷延時,當程式中存在多箇中斷時,容易鎖死,需要慎用。
如果在中斷服務程式裏面呼叫延遲函數 HAL_Delay 要特別注意,因爲這個函數的時間基準是基於滴答定時器或者其他通用定時器實現,實現方式是滴答定時器或者其他通用定時器裏面做了個變數計數。如此一來,結果是顯而易見的,如果其他中斷服務程式呼叫了此函數,且中斷優先順序高於滴答定時器,會導致滴答定時器中斷服務程式一直得不到執行(即變數計數值無法遞減0,導致HAL_Delay函數無法執行完),從而卡死在裏面。所以滴答定時器的中斷優先順序一定要比它們高。
一句話總結就是,呼叫HAL_Delay函數的中斷服務函數的中斷優先順序必須低於滴答定時器的優先順序或者低於HAL_Delay函數使用的定時器的優先順序。

3. CUBE生成的程式中, SysTick是中斷型延時(利用中斷來查詢時間到了沒)。

  /* Use systick as time base source and configure 1ms tick (default clock after Reset is MSI) */
  HAL_InitTick(TICK_INT_PRIORITY);



#define  TICK_INT_PRIORITY            ((uint32_t)0x000F)    /*!< tick interrupt priority */    

SysTick是內核中斷,優先級別預設最低。

(可以用內核函數來修改~ 當然,這就要看內核M3的書了,而不是看STM32的參考手冊那麼簡單。暫時就不深入研究,日後更新。)

總結起來就是,就是傳說中優先級別預設最低,雖然SysTick一直在跑,但是沒進入到中斷來讀取它的值~

(不知是哪裏讓我潛意識地認爲SysTick級別比外設都高,導致這問題)

如果中斷裡呼叫HAL_Delay就會停在那裏,因爲根本不會進入那個級別更低的中斷。

資料補充:

網上還有一種寫 法是時間摘取法,是一直讀取SysTick產生延時函數~(原子的例程就是用這種方法)
其次,有人提到,中斷裏面不應該使用延時,中斷所佔的時間越短越好有道理

附上原子的時間摘取法的程式,很有學習價值~

//////////////////////////////////////////////////////////////////////////////////	 
//本程式只供學習使用,未經作者許可,不得用於其它任何用途
//Mini STM32開發板
//使用SysTick的普通計數模式對延遲進行管理
//包括delay_us,delay_ms
//正點原子@ALIENTEK
//技術論壇:www.openedv.com
//修改日期:2010/5/27
//版本:V1.2
//版權所有,盜版必究。
//Copyright(C) 正點原子 2009-2019
//All rights reserved
//********************************************************************************
//V1.2修改說明
//修正了中斷中呼叫出現死回圈的錯誤
//防止延時不準 不準確,採用do while結構!
//////////////////////////////////////////////////////////////////////////////////	 
static u8  fac_us=0;//us延時倍乘數
static u16 fac_ms=0;//ms延時倍乘數
//初始化延遲函數
//SYSTICK的時鐘固定爲HCLK時鐘的1/8
//SYSCLK:系統時鐘
void delay_init(u8 SYSCLK)
{
	SysTick->CTRL&=0xfffffffb;//bit2清空,選擇外部時鐘  HCLK/8
	fac_us=SYSCLK/8;		    
	fac_ms=(u16)fac_us*1000;
}								    
//延時nms
//注意nms的範圍
//SysTick->LOAD爲24位元暫存器,所以,最大延時爲:
//nms<=0xffffff*8*1000/SYSCLK
//SYSCLK單位爲Hz,nms單位爲ms
//對72M條件下,nms<=1864 
void delay_ms(u16 nms)
{	 		  	  
	u32 temp;		   
	SysTick->LOAD=(u32)nms*fac_ms;//時間載入(SysTick->LOAD爲24bit)
	SysTick->VAL =0x00;           //清空計數器
	SysTick->CTRL=0x01 ;          //開始倒數  
	do
	{
		temp=SysTick->CTRL;
	}
	while(temp&0x01&&!(temp&(1<<16)));//等待時間到達   
	SysTick->CTRL=0x00;       //關閉計數器
	SysTick->VAL =0X00;       //清空計數器	  	    
}   
//延時nus
//nus爲要延時的us數.		    								   
void delay_us(u32 nus)
{		
	u32 temp;	    	 
	SysTick->LOAD=nus*fac_us; //時間載入	  		 
	SysTick->VAL=0x00;        //清空計數器
	SysTick->CTRL=0x01 ;      //開始倒數 	 
	do
	{
		temp=SysTick->CTRL;
	}
	while(temp&0x01&&!(temp&(1<<16)));//等待時間到達   
	SysTick->CTRL=0x00;       //關閉計數器
	SysTick->VAL =0X00;       //清空計數器	 
}

三、基於HAL實現非阻塞式定時器

1. 設定定時器1爲 ms 毫秒延時定時器

假如我們要看TIM1在哪個時鐘下,進入TIM1時鐘開啓函數即可
假如我们要看TIM1在哪个时钟下,进入TIM1时钟开启函数即可在这里插入图片描述

設定定時器1爲 ms 毫秒延時定時器
根據文章開頭的方法,定時器1使用APB2時鐘,主頻80MHz
在這裏插入圖片描述
在这里插入图片描述

設定TIM1參數,分頻係數10,000,計數頻率爲8KHz
在這裏插入圖片描述
在这里插入图片描述

生成工程,新增延時函數程式碼

void tx_delay_ms(uint16_t nms)	//量程0-8191ms
{
		__HAL_TIM_SetCounter(&htim1, 0);//htim1

		__HAL_TIM_ENABLE(&htim1);

		while(__HAL_TIM_GetCounter(&htim1) < (8 * nms));//計數頻率8KHz,8次即爲1ms
		/* Disable the Peripheral */
		__HAL_TIM_DISABLE(&htim1);
}

然後再在標頭檔案新增宣告即可。

2. 設定定時器8爲 us 微秒延時定時器

根據文章開頭的方法,定時器8使用APB2時鐘,主頻80MHz
在這裏插入圖片描述
在这里插入图片描述

設定TIM8參數,分頻係數10,計數頻率爲8MHz在這裏插入圖片描述
在这里插入图片描述

生成工程,新增延時函數程式碼

void tx_delay_us(uint16_t nus)	//量程0-8191us
{
		__HAL_TIM_SetCounter(&htim8, 0);//htim8

		__HAL_TIM_ENABLE(&htim8);

		while(__HAL_TIM_GetCounter(&htim8) < (8 * nus));//計數頻率8MHz,8次即爲1us
		/* Disable the Peripheral */
		__HAL_TIM_DISABLE(&htim8);
}

然後再在標頭檔案新增宣告即可

3 中斷方式延時函數

我們使用定時器3來產生中斷,定時器3在APB1下,80MHz
在这里插入图片描述

在這裏插入圖片描述
設定定時器3參數
在这里插入图片描述

編寫延時函數
(1) 首先宣告中斷累加變數

static volatile uint16_t tim3_counter;

(2)編寫延時函數延時時長(0-65535ms)

void tim3_delay_ms(uint16_t nms)
{
	tim3_counter = 0;
	while(tim3_counter!=nms);
}

(3)設定定時器中斷回撥函數

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if (htim->Instance == htim3.Instance)	//定時器3中斷 1ms/次
	{
		tim3_counter++;
	}
}

開始使用!