關於軟體定時器的一些討論

語言: CN / TW / HK

簡介

這裡先介紹下軟體定時器和硬體定時器的區別

硬體定時器:

CPU內部自帶的定時器模組,通過初始化、配置可以實現定時,定時時間到以後就會執行相應的定時器中斷處理函式。硬體定時器一般都帶有其它功能,比如PWM輸出、輸入捕獲等等功能。但是缺點是硬體定時器數量少!!

軟體定時器:

軟體定時器允許設定一段時間,當設定的時間到達之後就執行指定的功能函式,被定時器呼叫的這個功能函式叫做定時器的回撥函式。回撥函式的兩次執行間隔叫做定時器的定時週期,簡而言之,當定時器的定時週期到了以後就會執行回撥函式。

在FreeRTOS中有專門的軟體定時器功能,我們可以在MCU中簡單的是實現“軟體定時器”如下:

void timer_1000ms(void)
{
  printf("timer_1000ms\r\n");
}
int main(void)
{
  static timer_tick = 0;
  timer_tick = systick_ms;
  while()
  {
    if((systick_ms-timer_tick)>1000)
    {
      timer_tick = systick_ms;
      timer_1000ms();
    }
  }
}

這就是簡單的軟體定時器,是的,這就是特別簡潔版本的軟體定時器。當然它是有缺點的,比如systick_ms每1ms加1,所以軟體定時器的精度是ms為單位的,並且如果while(1)中有其他程式碼阻塞,軟體定時器也會跟著阻塞的。

這個簡單的軟體定時器畢竟很"簡陋",大家可以自行封裝豐富一下,或者參考已經有的開源方案:MultiTimer,一款可無限擴充套件的軟體定時器。

MultiTimer 是一個軟體定時器擴充套件模組,可無限擴充套件你所需的定時器任務,取代傳統的標誌位判斷方式, 更優雅更便捷地管理程式的時間觸發時序。

開源地址: https://github.com/0x1abin/MultiTimer。

MultiTimer

MultiTimer的設計比較簡潔,只有2個檔案,並且只有4個函式,總共82行程式碼,稍微花一點功夫就可以理解明白。

移植步驟

  • 配置系統時間基準介面,安裝定時器驅動
uint64_t PlatformTicksGetFunc(void)
{
    /* Platform implementation */
}
MultiTimerInstall(PlatformTicksGetFunc);
  • 例項化一個定時器物件
MultiTimer timer1;
  • 設定定時時間,超時回撥處理函式, 使用者上下指標,啟動定時器
int MultiTimerStart(&timer1, uint64_t timing, MultiTimerCallback_t callback, void* userData);
  • 在主迴圈呼叫定時器後臺處理函式
int main(int argc, char *argv[])
{
    ...
    while (1) {
        ...
        MultiTimerYield();
    }
}

具體就不做手把手教程如何移植了,在STM32F207移植好的工程開源地址:

開源地址:​ https://github.com/strongercjd/STM32F207VCT6/tree/master/23-Timer-MultiTimer。

下面分析一下MultiTimer。

在移植的第一步,配置系統時間基準介面,安裝定時器驅動。

uint64_t PlatformTicksGetFunc(void)
{
    /* Platform implementation */
}
MultiTimerInstall(PlatformTicksGetFunc);

看一下MultiTimerInstall函式原型。

typedef uint64_t (*PlatformTicksFunction_t)(void);
static PlatformTicksFunction_t platformTicksFunction = NULL;
int MultiTimerInstall(PlatformTicksFunction_t ticksFunc)
{
    platformTicksFunction = ticksFunc;
    return 0;
}

這其實就是函式指標實現的回撥函式,其實就是給MultiTimer提供一個計數器。

除去回撥函式,該開源專案還是單鏈表的很好的示例,學習資料結構是比較乏味的,這個開源專案是單鏈表很好的應用落地,不太懂的同學可以學習下。下面摘取一下部分程式碼

連結串列的刪除:

for (; *nextTimer; nextTimer = &(*nextTimer)->next) {
  if (timer == *nextTimer) {
    *nextTimer = timer->next; /* remove from list */
    break;
  }
}

插入連結串列:

for (nextTimer = &timerList;; nextTimer = &(*nextTimer)->next) {
  if (!*nextTimer) {
    timer->next = NULL;
    *nextTimer = timer;
    break;
  }
  if (timer->deadline < (*nextTimer)->deadline) {
    timer->next = *nextTimer;
    *nextTimer = timer;
    break;
  }
}

遍歷連結串列:

MultiTimer* entry = timerList;
for (; entry; entry = entry->next) {
  /* Sorted list, just process with the front part. */
  if (platformTicksFunction() < entry->deadline) {
    return (int)(entry->deadline - platformTicksFunction());
  }
  /* remove expired timer from list */
  timerList = entry->next;
  /* call callback */
  if (entry->callback) {
    entry->callback(entry, entry->userData);
  }
}

當然MultiTimer也是有缺點的,比如一個定時器是1000ms,另一個定時器是500ms,排程時就會衝突,也沒有定時器排程搶佔,會隨著其他程式碼的阻塞而阻塞。這種類似的問題不再詳述,大家使用的時候多測測就好。

任務排程

看了上面的操作,如果我們不叫軟體定時器,叫它“任務”,是不是和FreeRTOS任務類似,感覺高階一些,甚至這篇文章標題可以修改為《一篇文章教你實現作業系統》,開個歡笑,不做標題黨。

有些專案實時性要求高,需要任務搶佔,所以需要使用FreeRTOS這樣的作業系統,但它資源佔用比例過大,不利於專案開發,在一般的小專案中也用不到RTOS的太多功能,使用上面的思路,你可以把每個任務設定不同的間隔時間週期性呼叫,如果有實時性要求很高的事件,就通過中斷處理。

當然也可以使用開頭的粗糙方法:

if((systick_ms-timer_tick)>1000)
{
   timer_tick = systick_ms;
   timer_1000ms();
}

這樣功能是可以實現的,但沒有模組化,不利於程式碼的維護。我們可以借鑑MultiTimer思路封裝一下軟體介面。

並且,如果你的專案中,任務的個數是固定不變的,可以將MultiTimer中的連結串列拿掉,直接使用全域性變數就可以,如果有額外的時間模仿FreeRTOS實現一些訊號量,對列等,這就是自己的OS(無搶佔)啊。(當然這屬於重複造輪子,但對一些公司來講,有適合自己業務的,最精簡的程式碼框架是很有必要的)。

本文轉載自微信公眾號「知曉程式設計」,可以通過以下二維碼關注。轉載本文請聯絡知曉程式設計公眾號。