藍牙芯片NRF51822入門學習:時間管理

前言
如果你跟我一樣是一個藍牙新手,並且還沒有買nrf51822的開發板的話,推薦先學cc2541,如果已經買了開發板,那就看我的《藍牙芯片NRF51822入門學習》系列文章吧,祝你儘早出坑。

本文面對的是:已經掌握nrf51822基本外設,並且閱讀了TI官方視頻,對藍牙有了初步瞭解,但對怎麼進一步學習nrf51822沒有頭緒的孩子。

本文的相關工具、代碼和文章更新網盤鏈接:http://pan.baidu.com/s/1bn5y9gr 密碼:ijxf
網盤內有以下兩個子文件夾:

\相關軟件 當中nrf51_sdk_v6_0_0_43681.zip是我們要用到的V6.0版本的SDK
\文章更新 內部有每篇文章的PDF、代碼、資料、工具。
\BLE視頻 TI對藍牙基本屬性的講解

開發工具

5.1版MDK,nRFgo studio

固件版本

nrf51_sdk_v6_0_0_43681.msi,s110_nrf51822_7.0.0_softdevice.hex

相關硬件

JTAG,兼容pca10001的nrf51822開發板。

第一節:常用接口描述

開篇教程中,我們來學習nrf51822的Libraries中時間管理模塊,它的源代碼和頭文件分別爲app_timer.c/app_timer.h

這是Nordic爲我們提供的虛擬定時器,這個定時器不同於硬件上的TIMER,而是基於RTC1實現的一種虛擬定時器,其將定時功能作爲了一個資源進行管理,所以會有初始化、創建等過程。

  • PS1:
    nrf51822的SDK採用封裝思想,需要暴露給用戶的信息都在相關模塊的頭文件中;爲了提醒用戶不去看具體實現細節,我們可以發現相關的API,比如app_timer_create()的源碼部分都是沒有接口描述信息的,相關使用方法需要看app_timer.h或者翻閱SDK目錄下的Documentation\index.html。還有一些隱藏細節的封裝技巧,感興趣的孩童可以看本篇第三節。

  • PS2:快速查找定位文件,可以安裝一個軟件:everything,輸入文件名就可以瞬間查找到想要的文件,然後再文件上右鍵選擇“open
    path”就可以打開文件所在的文件夾了。

1、參數宏APP_TIMER_INIT()

這個宏用於初始化app_timer模塊,這是一個參數宏,接口定義如下:

APP_TIMER_INIT(PRESCALER, MAX_TIMERS, OP_QUEUES_SIZE, USE_SCHEDULER)

其中PRESCALE 分頻比例,填入0的話,每秒就產生32768次tick,定時最大長度爲0xFFFFFF次tick,也就是說500多秒定時。

  • PS3:與ucos提供的時基tick不同,本SDK的主要在定時到達的時候進入RTC中斷,而不是每個TICK都進入。因此就算每秒就產生32768次tick,也不會拖慢系統性能。

MAX_TIMERS 必須大於等於工程中創建的timer數量。

OP_QUEUES_SIZE 操作隊列的大小,具體意思看第三節。如果不作死,選擇等於MAX_TIMERS就行了。

USE_SCHEDULER 是否使用任務調度器,當前不使用

2、參數宏APP_TIMER_TICKS()

這個宏用於計算特定毫秒數相當於多少個tick。接口定義如下:

APP_TIMER_TICKS(MS, PRESCALER)

其中MS是單位爲毫秒的定時時間,PRESCALER是分頻比例。
3、函數app_timer_create()

用於創建一個timer,並獲取生成timer的控制句柄。接口定義如下:

uint32_t app_timer_create(app_timer_id_t *            p_timer_id,

                          app_timer_mode_t            mode,

                          app_timer_timeout_handler_t timeout_handler)

p_timer_id 讀取到創建的timer的句柄

mode timer的類型,其中


                  APP_TIMER_MODE_SINGLE_SHOT是單次執行

                  APP_TIMER_MODE_REPEATED是循環執行

				  timeout_handler    被註冊到內核的回調函數,當timer超時後就會執行。

4、函數app_timer_start()

設置一個timer的定時間隔和上下文參數,並啓動這個timer。接口定義如下:

uint32_t app_timer_start(app_timer_id_t timer_id, uint32_t timeout_ticks, void * p_context)

timer_id_t app_timer_create()裏創建的timer句柄

timeout_ticks 定時的tick數量,一般用APP_TIMER_TICKS()計算。

p_context 傳遞給超時回調函數的參數,不能指向局部的自動變量。

5、app_timer_stop()

停止一個timer的運行。接口定義如下:


uint32_t app_timer_stop(app_timer_id_t timer_id)

timer_id app_timer_create()裏創建的timer句柄

第二節:流水燈例子

討論延時最簡單的例子就是流水燈了,代碼位於網盤目錄下的\文章更新\藍牙芯片NRF51822入門學習1:時間管理.rar\ Timer_Blinky.rar,在SDK所在目錄\nrf51822下新建文件夾Test,將Timer_Blinky.rar解壓到文件夾裏。
打開Timer_Blinky\MDK\ timer_blink.uvproj

實驗代碼位於main.c內

首先定義幾個初始化用到的宏


#define APP_TIMER_PRESCALER         0  

#define APP_TIMER_MAX_TIMERS        1  

#define APP_TIMER_OP_QUEUE_SIZE     1  

在TimerInit()中啓用LFCLK並初始化timer(官方例程ble_app_uart沒有開啓LFCLLK的動作,也許S110的底層會自動開啓LFCLK時鐘)

    NRF_CLOCK->EVENTS_LFCLKSTARTED = 0;

    NRF_CLOCK->TASKS_LFCLKSTART = 1;

    while(NRF_CLOCK->EVENTS_LFCLKSTARTED == 0)

    {

        //donoting

    }

    NRF_CLOCK->EVENTS_LFCLKSTARTED = 0;

    APP_TIMER_INIT(APP_TIMER_PRESCALER, APP_TIMER_MAX_TIMERS, APP_TIMER_OP_QUEUE_SIZE, false);

然後在BlinkInit()內創建一個流水燈所用到的timer,相關代碼是

    uint32_t ERR_CODE = app_timer_create(&BlinkHandle, APP_TIMER_MODE_SINGLE_SHOT, BlinkTimeoutHandler);
    //創建定時器,並讀取句

柄號,單次執行

  APP_ERROR_CHECK(ERR_CODE);

其中BlinkHandle是用於保存句柄號的全局變量。

BlinkTimeoutHandler是爲流水燈編寫的超時回調函數,在這個函數內判斷當前LED燈是否點亮,是否需要操作下一個LED燈,並通過app_timer_start()設置下一輪的定時時間。

最後通過BlinkStart()來啓動流水燈的timer。

void BlinkStart(bool IsForward)

{

    static bool ForwardFlag;

    ForwardFlag = IsForward;

    ……

    app_timer_start(BlinkHandle, LED_ON_TIME, (void*)&ForwardFlag);

}

最後mian()的執行順序如下:

int main()

{

    TimerInit();

    BlinkInit();

    BlinkStart(true);

    while(1)

    {

//        BlinkStart(false);

//        nrf_delay_ms(4000);

//        BlinkStart(true);

//        nrf_delay_ms(4000);

    }

}

下載程序到開發板後,可以觀察到LED燈依次被點亮。

第三節:app_timer具體實現的源碼閱讀提示

本節送給喜(te)歡(bie)專(dan)研(teng)的孩子,本節只對app_time模塊做個粗略的概述,當對模塊有了宏觀的理解後,再讀源代碼就能事半功倍。

  • PS4:推薦使用source insight閱讀源碼,高亮標記(shift+F8)功能非常有用,本教程均提供了source
    insight的工程文件夾。

section1:

我們打開app_timer.h

#define APP_TIMER_NODE_SIZE          40                        

#define APP_TIMER_USER_OP_SIZE       24                        

#define APP_TIMER_USER_SIZE          8                         

#define APP_TIMER_INT_LEVELS         3                         

這裏APP_TIMER_NODE_SIZE、APP_TIMER_USER_OP_SIZE、APP_TIMER_USER_SIZE分別聲明的是timer_node_t、timer_user_op_t、timer_user_t三個結構體的大小,用於參數宏APP_TIMER_INIT()內部分配數據空間。這樣的話就無需將這三個結構體的內部細節暴露給用戶了。而在源碼中使用靜態斷言STATIC_ASSERT()來判斷這三個宏的大小是否正確。

APP_TIMER_INT_LEVELS指的是SDK定義的三個中斷等級,APP_IRQ_PRIORITY_LOW(低優先級3)、APP_IRQ_PRIORITY_HIGH(高優先級1)、NRF_APP_PRIORITY_THREAD(用戶優先級!=[1或者3])。

section2:
app_timer模塊主要佔用內存的分配方式,這些內存的分配在APP_TIMER_INIT()中實現,分別爲timer節點(類型timer_node_t)、操作隊列對象(類型timer_user_t)和操作隊列存儲空間(類型timer_user_op_t)分配空間。

假定APP_TIMER_INIT()的參數MAX_TIMERS2, OP_QUEUES_SIZE4則它們的內存映射如下圖所示。
圖一
節點nodex就是我們創建的timer的對象,而由userx同對應的opx組成的隊列則是保存相關操作(比如start或者stop操作)的地方。初始化函數同時會給p_buffer、mp_users這兩個全局變量初始化,讓它們分別指向首個timer節點和首個操作隊列對象。這樣後面就能用

p_buffer[xx]的方式找到對應的對象了。
section3:延時鏈表

timer節點的類型爲timer_node_t,其內部有一個next屬性,這是用來實現延時鏈表的。當相應的timer被start後,它的node就會被加入到延時鏈表中

在這裏插入圖片描述
如上圖所示,全局變量m_timer_id_head是延時鏈表的表頭,當表頭指向TIMER_NULL時候證明延時鏈表爲空。每個node都有一個ticks_to_expire屬性,作用如下:
在這裏插入圖片描述
如上圖所示,nodeA的ticks_to_expir指示了nodeA與上次超時(m_ticks_latest)之間的時間差;nodeB的ticks_to_expir則指示了與nodeA之間的時間差。

RTC1_Counter是app_timer模塊選用的底層RTC的計數器,當它自增到nodeA標記的值後,就會觸發一次RTC中斷,在中斷中執行nodeA內部所註冊的超時回調函數並更新m_ticks_latest的值後,觸發進入SWI0中斷中;在SWI0中斷內將nodeA從鏈表中刪掉,並根據m_ticks_latest和nodeB的ticks_to_expire計算出新的NRF_RTC1->CC[0]值,用於定時時間到達後觸發新的RTC中斷。

假設用戶用過app_timer_start()指示需要將一個timer對應nodeE加入到鏈表中,則會通過記錄下來請求插入時候RTC1_Counter的值(記錄於ticks_at_start)和定時間隔(記錄於ticks_first_interval),計算插入到鏈表的那個位置。

而因爲已經執行而被刪掉的node,如果節點模式爲APP_TIMER_MODE_REPEATED,也會重新插回到鏈表中。
section4:操作隊列

在app_timer中有3個op隊列,對應 APP_IRQ_PRIORITY_LOW、APP_IRQ_PRIORITY_HIGH、NRF_APP_PRIORITY_THREAD三個優先等級。

當用戶調用app_timer_start()或者app_timer_stop()時候,相關的一些對延時鏈表的操作並不是立即執行的。而是通過FIFO操作寫入到當前環境對應的op隊列中(在user_id_get()裏通過SCB->ICSR寄存獲取當前運行環境,可以參考《STM32F10X-programming-manual.pdf》第134頁的描述),然後觸發軟中斷SWI0,在軟中斷中讀取3個隊列中的操作數據,對延時鏈表內對應的節點進行操作。

section5:相關中斷

app_timer分別使用了兩個中斷:RTC1和SWI0,其中SWI0是軟中斷,用戶可以分別通過NVIC_SetPendingIRQ(RTC1_IRQn)和NVIC_SetPendingIRQ(SWI0_IRQn)直接觸發。

其中RTC1_IRQHandler()調用timer_timeouts_check()處理超時的情況,而SWI0_IRQHandler()調用timer_list_handler()對鏈表進行處理。當在timer_timeouts_check()內部需要對鏈表進行修改時候,就會觸發SWI0來處理鏈表問題,反之亦然。

因爲timer_timeouts_check()和timer_list_handler()都對鏈表進行操作,因此要求他們的中斷優先等級必須一致,app_timer中默認都是APP_IRQ_PRIORITY_LOW級。

有明白的可以加羣:805601459(備註CSDN)進行交流。本文僅限於內部交流,禁止商用!

發佈了11 篇原創文章 · 獲贊 7 · 訪問量 2418
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章