痞子衡嵌入式:嵌入式裡通用微秒(microseconds)計時函式框架設計與實現

痞子衡 發表於 2021-06-09

  大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是嵌入式裡通用微秒(microseconds)計時函式框架設計與實現

  在嵌入式軟體開發裡,計時可以說是非常基礎的功能模組了,其應用也非常廣泛,比如可以輔助計算訊號脈衝寬度時間,也可以直接用於常規延時等。相信很多人初次領略 MCU 的神奇都是從計時功能相關小程式開始的。

  在 MCU 裡要想實現精確計時,往往都是利用其內部硬體定時器。不同廠商的 MCU,其定時器設計與使用都不太一樣。即使是同一 MCU 內,通常也會有好幾種不同型別的定時器共存。基於此,痞子衡今天分享一種非常簡單實用的通用計時函式框架,這個框架的目的是統一計時函式介面,並且在實現上將通用部分和硬體相關部分剝離開,這樣你的嵌入式專案在使用這個框架時可以無縫快捷地切換底層定時器。

  注:本框架主要適合定時器時鐘源不小於 1MHz 的 MCU,因為函式介面裡延時最小單元是 1us。對於一些定時器時鐘源低於 1MHz 的 MCU,可將本框架簡單改成成毫秒(milliseconds)計時函式。

一、微秒(microseconds)計時函式庫設計

1.1 函式介面定義

  首先是設計通用計時函式框架標頭檔案:microseconds.h ,這個標頭檔案裡直接定義如下 7 個介面函式原型。涵蓋必備的初始化流程init()、shutdown(),最核心的計時功能get_ticks()、convert_to_microseconds(),常用的延時功能delay()、set_delay()、is_timeout()。

//! @brief 初始化計時
void microseconds_init(void);
//! @brief 關閉計時
void microseconds_shutdown(void);
//! @brief 獲取系統累計計數值
uint64_t microseconds_get_ticks(void);
//! @brief 將計數值轉換為時間值(微秒)
uint32_t microseconds_convert_to_microseconds(uint64_t ticks);
//! @brief 阻塞型延時(微秒級)
void microseconds_delay(uint32_t us);
//! @brief 設定超時時間(用於非阻塞型延時)
void microseconds_set_delay(uint32_t us);
//! @brief 判斷是否超時(用於非阻塞型延時)
bool microseconds_is_timeout(void);

1.2 通用函式實現

  然後是設計通用計時函式框架共用原始檔:microseconds_common.c,這個檔案裡涉及三個靜態全域性變數定義,四個私有函式宣告,以及除了 get_ticks() 之外的 6 個介面函式實現。

  其中 s_tickPerMicrosecond 變數存的是每微秒對應計數值,其實這個變數不是一定要定義的,可以在函式需要時實時計算,但為了小小提升框架效能,就在 init() 裡將這個值先算出來了,方便其他函式直接使用。

  s_highCounter 變數存的是定時器中斷次數,即高位計數器,因為框架 get_ticks() 介面返回的是 64bit 的計數值,對於有些寬度小於 32bit 的定時器,我們常常需要開啟定時器中斷,否則無法保證系統長時間執行線性計時的正確性(比如 100MHz 時鐘源的 32bit 定時器,最長約 43 秒就會清零翻轉一次,需要 s_highCounter 變數記錄翻轉次數)。當然如果 MCU 裡能級連出 64bit 的定時器,就可以不用開啟中斷(清零翻轉的時間特別長,可近似認為是永久),s_highCounter 此時就不需要了。

  關於延時函式介面,delay() 用於阻塞型延時,即呼叫這個函式後一定是死等指定時間後才退出,系統會被強制掛起;set_delay()/is_timeout()用於非阻塞型延時,系統可以繼續幹其他任務,在需要的時侯來檢視一下超時時間是否到了即可。兩種延時各有各的用途。

//!< 每微秒等效計數值
static uint32_t s_tickPerMicrosecond;
//!< 超時時間點對應系統計數值(用於非阻塞型延時)
static uint64_t s_timeoutTicks;
//!< 高位計數器,僅當使能定時器超時中斷時有效,用於記錄中斷累計次數
volatile uint32_t s_highCounter;

//! @brief 開啟硬體定時器
extern void microseconds_timer_init(void);
//! @brief 關閉硬體定時器
extern void microseconds_timer_deinit(void);
//! @brief 獲取定時器時鐘源數值
extern uint32_t microseconds_get_clock(void);
//! @brief 將時間值(微秒)轉換為計數值
static uint64_t microseconds_convert_to_ticks(uint32_t microseconds);

void microseconds_init(void)
{
    // 清零高位計數器
    s_highCounter = 0;
    // 開啟硬體定時器
    microseconds_timer_init();
    // 計算每微秒的等效計數值
    s_tickPerMicrosecond = microseconds_get_clock() / 1000000UL;
    // 假設定時器時鐘源不小於 1MHz
    assert(s_tickPerMicrosecond);
}

void microseconds_shutdown(void)
{
    // 關閉硬體定時器
    microseconds_timer_deinit();
}

uint32_t microseconds_convert_to_microseconds(uint64_t ticks)
{
    return (ticks / s_tickPerMicrosecond);
}

uint64_t microseconds_convert_to_ticks(uint32_t microseconds)
{
    return ((uint64_t)microseconds * s_tickPerMicrosecond);
}

void microseconds_delay(uint32_t us)
{
    // 獲取系統當前計數值
    uint64_t currentTicks = microseconds_get_ticks();
    // 計算超時時間點系統計數值
    uint64_t ticksNeeded = ((uint64_t)us * s_tickPerMicrosecond) + currentTicks;
    // 等待系統計數值到達超時時間點系統計數值
    while (microseconds_get_ticks() < ticksNeeded);
}

void microseconds_set_delay(uint32_t us)
{
    // 計算超時時間等效計數值
    uint64_t ticks = microseconds_convert_to_ticks(us);
    // 設定超時時間點系統計數值
    s_timeoutTicks = microseconds_get_ticks() + ticks;
}

bool microseconds_is_timeout(void)
{
    // 獲取系統當前計數值
    uint64_t currentTicks = microseconds_get_ticks();
    // 判斷系統計數值是否大於超時時間點系統計數值
    return (currentTicks < s_timeoutTicks) ? false : true;
}

二、微秒(microseconds)計時函式庫實現

2.1 定時器相關實現(基於Cortex-M核心的SysTick)

  最後是設計 MCU 相關的通用計時函式框架原始檔:microseconds_xxTimer.c,這裡我們以 Cortex-M 系列 MCU 的核心定時器 SysTick 為例。

  SysTick 是 24bit 遞減定時器,時鐘源有兩種配置:一是核心主頻,二是外部時鐘(看廠商實現),最常用的時鐘源配置就是與核心同頻。

  上一節說了用 SysTick 這類寬度小於 32bit 的定時器,是需要開啟定時器中斷的,所以 s_highCounter 會生效。get_ticks()是整個計時函式框架裡最基礎也最核心的功能介面,這裡面的實現有一個需要特別注意的地方,就是取系統當前計數值可能會有數值回退的風險,需要使用程式碼中 do {} while();方式來確保正確性。

//!< 高位計數器,僅當使能定時器超時中斷時有效,用於記錄中斷累計次數
extern volatile uint32_t s_highCounter;

void microseconds_timer_init(void)
{
    // 呼叫 core_cmx.h 標頭檔案裡的初始化函式
    // SysTick時鐘源為核心時鐘,開啟中斷,重灌值為 0xFFFFFF
    SysTick_Config(SysTick_LOAD_RELOAD_Msk + 1);
}

void microseconds_timer_deinit(void)
{
    SysTick->CTRL &= ~(SysTick_CTRL_CLKSOURCE_Msk |
                       SysTick_CTRL_TICKINT_Msk |
                       SysTick_CTRL_ENABLE_Msk);
    SysTick->VAL = 0;
}

uint32_t microseconds_get_clock(void)
{
    return SystemCoreClock;
}

uint64_t microseconds_get_ticks(void)
{
    uint32_t high;
    uint32_t low;
    // 這裡的實現要注意確保中斷髮生時獲取系統累計計數值的正確性
    do
    {
        // 先快取高位計數器
        high = s_highCounter;
        // 再讀定時器實際計數值
        low = ~SysTick->VAL & SysTick_LOAD_RELOAD_Msk;
    } while (high != s_highCounter); // 保證快取高位值與讀實際低位值間隙中沒有發生中斷

    return ((uint64_t)high << 24) + low;
}

void SysTick_Handler(void)
{
    s_highCounter++;
}

  當然還有很多具體 MCU 平臺的各種定時器實現,因此這個專案會不斷更新,也歡迎大家來參與貢獻。

  至此,嵌入式裡通用微秒(microseconds)計時函式框架設計與實現痞子衡便介紹完畢了,掌聲在哪裡~~~

歡迎訂閱

文章會同時釋出到我的 部落格園主頁CSDN主頁知乎主頁微信公眾號 平臺上。

微信搜尋"痞子衡嵌入式"或者掃描下面二維碼,就可以在手機上第一時間看了哦。

痞子衡嵌入式:嵌入式裡通用微秒(microseconds)計時函式框架設計與實現