1. 程式人生 > >一種嵌入式系統軟體定時器的實現:以STM32為例

一種嵌入式系統軟體定時器的實現:以STM32為例


1.什麼是軟體定時器

軟體定時器是用程式模擬出來的定時器,可以由一個硬體定時器模擬出成千上萬個軟體定時器,這樣程式在需要使用較多定時器的時候就不會受限於硬體資源的不足,這是軟體定時器的一個優點,即數量不受限制。但由於軟體定時器是通過程式實現的,其執行和維護都需要耗費一定的CPU資源,同時精度也相對硬體定時器要差一些。


2.軟體定時器的實現原理

在Linux,uC/OS,FreeRTOS等作業系統中,都帶有軟體定時器,原理大同小異。典型的實現方法是:通過一個硬體定時器產生固定的時鐘節拍,每次硬體定時器中斷到,就對一個全域性的時間標記加一,每個軟體定時器都儲存著到期時間,程式需要定期掃描所有執行中的軟體定時器,將各個到期時間與全域性時鐘標記做比較,以判斷對應軟體定時器是否到期,到期則執行相應的回撥函式,並關閉該定時器。

以上是單次定時器的實現,若要實現週期定時器,即到期後接著重新定時,只需要在執行完回撥函式後,獲取當前時間標記的值,加上延時時間作為下一次到期時間,繼續執行軟體定時器即可。


3.基於STM32的軟體定時器

3.1 時鐘節拍

軟體定時器需要一個硬體時鐘源作為基準,這個時鐘源有一個固定的節拍(可以理解為秒針的每次滴答),用一個32位的全域性變數tickCnt來記錄這個節拍的變化:

static volatile uint32_t tickCnt = 0;   	//軟體定時器時鐘節拍

每來一個節拍就對tickCnt加一(記錄滴答了多少下):

/* 需在定時器中斷內執行 */
void tickCnt_Update
(void) { tickCnt++; }

一旦開始執行,tickCnt將不停地加一,而每個軟體定時器都記錄著一個到期時間,只要tickCnt大於該到期時間,就代表定時器到期了。

3.2 資料結構

軟體定時器的資料結構決定了其執行的效能和功能,一般可分為兩種:陣列結構和連結串列結構。什麼意思呢?這是(多個)軟體定時器在記憶體中的儲存方式,可以用陣列來存,也可以用連結串列來存。

兩者的優劣之分就是兩種資料結構的特性之分:陣列方式的定時器查詢較快,但數量固定,無法動態變化,陣列大了容易浪費記憶體,陣列小了又可能不夠用,適用於定時事件明確且固定的系統;連結串列方式的定時器數量可動態增減,易造成記憶體碎片(如果沒有記憶體管理),查詢的時間開銷相對陣列大,適用於通用性強的系統,Linux,uC/OS,FreeRTOS等作業系統用的都是連結串列式的軟體定時器。

本文使用陣列結構:

static softTimer timer[TIMER_NUM];        //軟體定時器陣列

陣列和連結串列是軟體定時器整體的資料結構,當具體到單個定時器時,就涉及軟體定時器結構體的定義,軟體定時器所具有的功能與其結構體定義密切相關,以下是本文中軟體定時器的結構體定義:

typedef struct softTimer {
	uint8_t state;           //狀態
	uint8_t mode;            //模式
	uint32_t match;          //到期時間
	uint32_t period;         //定時週期
	callback *cb;            //回撥函式指標
	void *argv;              //引數指標
	uint16_t argc;           //引數個數
}softTimer;

定時器的狀態共有三種,預設是停止,啟動後為執行,到期後為超時。

typedef enum tmrState {
	SOFT_TIMER_STOPPED = 0,  //停止
	SOFT_TIMER_RUNNING,      //執行
	SOFT_TIMER_TIMEOUT       //超時
}tmrState;

模式有兩種:到期後就停止的是單次模式,到期後重新定時的是週期模式。

typedef enum tmrMode {
	MODE_ONE_SHOT = 0,       //單次模式
	MODE_PERIODIC,           //週期模式
}tmrMode;

不管哪種模式,定時器到期後,都將執行回撥函式,以下是該函式的定義,引數指標argv為void指標型別,便於傳入不同型別的引數。

typedef void callback(void *argv, uint16_t argc);

上述結構體中的模式state和回撥函式指標cb是可選的功能,如果系統不需要週期執行的定時器,或者不需要到期後自動執行某個函式,可刪除此二者定義。

3.3 定時器操作

3.3.1 初始化

首先是軟體定時器的初始化,對每個定時器結構體的成員賦初值,雖說static變數的初值為0,但個人覺得還是有必要保持初始化變數的習慣,避免出現一些奇奇怪怪的BUG。

void softTimer_Init(void)
{
	uint16_t i;
	for(i=0; i<TIMER_NUM; i++) {
		timer[i].state = SOFT_TIMER_STOPPED;
		timer[i].mode = MODE_ONE_SHOT;
		timer[i].match = 0;
		timer[i].period = 0;
		timer[i].cb = NULL;
		timer[i].argv = NULL;
		timer[i].argc = 0;
	}
}

3.3.2 啟動

啟動一個軟體定時器不僅要改變其狀態為執行狀態,同時還要告訴定時器什麼時候到期(當前tickCnt值加上延時時間即為到期時間),單次定時還是週期定時,到期後執行哪個函式,函式的引數是什麼,交代好這些就可以開跑了。

void softTimer_Start(uint16_t id, tmrMode mode, uint32_t delay, callback *cb, void *argv, uint16_t argc)
{
	assert_param(id < TIMER_NUM);
	assert_param(mode == MODE_ONE_SHOT || mode == MODE_PERIODIC);
	
	timer[id].match = tickCnt_Get() + delay;
	timer[id].period = delay;
	timer[id].state = SOFT_TIMER_RUNNING;
	timer[id].mode = mode;
	timer[id].cb = cb;
	timer[id].argv = argv;
	timer[id].argc = argc;
}

上面函式中的assert_param()用於引數檢查,類似於庫函式assert()。

3.3.3 更新

本文中軟體定時器有三種狀態:停止,執行和超時,不同的狀態做不同的事情。停止狀態最簡單,啥事都不做;執行狀態需要不停地檢查有沒有到期,到期就執行回撥函式並進入超時狀態;超時狀態判斷定時器的模式,如果是週期模式就更新到期時間,繼續執行,如果是單次模式就停止定時器。這些操作都由一個更新函式來實現:

void softTimer_Update(void)
{
	uint16_t i;
	
	for(i=0; i<TIMER_NUM; i++) {
	  switch (timer[i].state) {
          case SOFT_TIMER_STOPPED:
			  break;
		
		  case SOFT_TIMER_RUNNING:
			  if(timer[i].match <= tickCnt_Get()) {
				  timer[i].state = SOFT_TIMER_TIMEOUT;
				  timer[i].cb(timer[i].argv, timer[i].argc);       //執行回撥函式
			  }
			  break;
			
		  case SOFT_TIMER_TIMEOUT:
			  if(timer[i].mode == MODE_ONE_SHOT) {
			      timer[i].state = SOFT_TIMER_STOPPED;
			  } else {
				  timer[i].match = tickCnt_Get() + timer[i].period;
			      timer[i].state = SOFT_TIMER_RUNNING;
			  }
			  break;
		
		  default:
			  printf("timer[%d] state error!\r\n", i);
			  break;
	  }
  }
}

3.3.4 停止

如果定時器跑到一半,想把它停掉,就需要一個停止函式,操作很簡單,改變目標定時器的狀態為停止即可:

void softTimer_Stop(uint16_t id)
{
	assert_param(id < TIMER_NUM);
	timer[id].state = SOFT_TIMER_STOPPED;
}

3.3.5 讀狀態

又如果想知道一個定時器是在跑著呢還是已經停下來?也很簡單,返回它的狀態:

uint8_t softTimer_GetState(uint16_t id)
{
	return timer[id].state;
}

或許這看起來很怪,為什麼要返回,而不是直接讀?別忘了在前面3.2節中定義的定時器陣列是個靜態全域性變數,該變數只能被當前原始檔訪問,當外部檔案需要訪問它的時候只能通過函式返回,這是一種簡單的封裝,保持程式的模組化。

3.4 測試

最後,當然是來驗證一下我們的軟體定時器有沒達到預想的功能。定義三個定時器:定時器TMR_STRING_PRINT只執行一次,1s後在串列埠1列印一串字元;定時器TMR_TWINKLING為週期定時器,週期為0.5s,每次到期都將取反LED0的狀態,實現LED0的閃爍;定時器TMR_DELAY_ON執行一次,3s後點亮LED1,跟第一個定時器不同的是,此定時器的回撥函式是個空函式nop(),點亮LED1的操作通過主迴圈中判斷定時器的狀態來實現,這種方式在某些場合可能會用到。

static uint8_t data[] = {1,2,3,4,5,6,7,8,9,0};

int main(void)
{
	USART1_Init(115200);
	TIM4_Init(TIME_BASE_MS);
	TIM4_NVIC_Config();
	LED_Init();
	
	printf("I just grabbed a spoon.\r\n");
	
	softTimer_Start(TMR_STRING_PRINT, MODE_ONE_SHOT, 1000, stringPrint, data, 5);
	softTimer_Start(TMR_TWINKLING, MODE_PERIODIC, 500, LED0_Twinkling, NULL, 0);
	softTimer_Start(TMR_DELAY_ON, MODE_ONE_SHOT, 3000, nop, NULL, 0);
	
	while(1) {
		softTimer_Update();
		if(softTimer_GetState(TMR_DELAY_ON) == SOFT_TIMER_TIMEOUT) {
			LED1_On();
		}
	}
}

以下是測試結果,這是串列埠的列印:
串列埠列印

這是兩個LED:
LED0,LED1

請原諒我比較窮,LED1只能看電平輸出了,如下圖所示,輸出為低電平,嗯,確實亮了 :p
LED1

最後的最後,秉承分享精神,需要原始碼的朋友請猛戳這裡,歡迎找茬。


4.參考連結


2018/11/12   晴   SZ