以下轉載自安富萊電子: http://forum.armfly.com/forum.php
本章節開始講解 FreeRTOS 任務間的同步和資源共享機制,計數信號量。 FreeRTOS 中計數信號量的源碼實現是基於消息隊列實現的。
信號量的概念及其作用
信號量(semaphores)是 20 世紀 60 年代中期 Edgser Dijkstra 發明的。 使用信號量的最初目的是
為了給共享資源建立一個標志,該標志表示該共享資源被占用情況。這樣,當一個任務在訪問共享資源之
前,就可以先對這個標志進行查詢,從而在了解資源被占用的情況之后,再來決定自己的行為。
實際的應用中,信號量的作用又該如何體現呢?比如有個 30 人的電腦機房,我們就可以創建信號量
的初始化值是 30,表示 30 個可用資源,不理解的初學者表示信號量還有初始值?是的,信號量說白了就
是共享資源的數量。 另外我們要求一個同學使用一台電腦,這樣每有一個同學使用一台電腦,那么信號量
的數值就減一,直到 30 台電腦都被占用,此時信號量的數值就是 0。 如果此時還有幾個同學沒有電腦可
以使用,那么這幾個同學就得等待,直到有同學離開。 有一個同學離開,那么信號量的數值就加 1,有兩
個就加 2,依此類推。剛才沒有電腦用的同學此時就有電腦可以用了,有幾個同學用,信號量就減幾,直
到再次沒有電腦可以用,這么一個過程就是使用信號量來管理共享資源的過程。
平時使用信號量主要實現以下兩個功能:
兩個任務之間或者中斷函數跟任務之間的同步功能,這個和前面章節講解的事件標志組是類似的。其
實就是共享資源為 1 的時候。
多個共享資源的管理,就像上面舉的機房上機的例子。
針對這兩種功能,FreeRTOS 分別提供了二值信號量和計數信號量,其中二值信號量可以理解成計數
信號量的一種特殊形式,即初始化為僅有一個資源可以使用,只不過 FreeRTOS 對這兩種都提供了 API
函數,而像 RTX,uCOS-II 和 III 是僅提供了一個信號量功能,設置不同的初始值就可以分別實現二值信
號量和計數信號量。 當然,FreeRTOS 使用計數信號量也能夠實現同樣的效果。
實際上信號量還有很多其它用法,而且極具挑戰性,可以大大的開拓大家的視野,有興趣的同學可以
閱讀一下《The Little Book Of Semaphores》 ,作者是 Allen B. Downy。
FreeRTOS 任務間計數信號量的實現
任務間信號量的實現是指各個任務之間使用信號量實現任務的同步或者資源共享功能。 下面我們通過
如下的框圖來說明一下 FreeRTOS 計數信號量的實現,讓大家有一個形象的認識。 
運行條件:
創建 2 個任務 Task1 和 Task2。
創建計數信號量可用資源為 1。
運行過程描述如下:
任務 Task1 運行過程中調用函數 xSemaphoreTake 獲取信號量資源,如果信號量沒有被任務 Task2
占用,Task1 將直接獲取資源。 如果信號量被 Task2 占用,任務 Task1 將由運行態轉到阻塞狀態,
等待資源可用。一旦獲取了資源並使用完畢后會通過函數 xSemaphoreGive 釋放掉資源。
任務 Task2 運行過程中調用函數 xSemaphoreTake 獲取信號量資源,如果信號量沒有被任務 Task1
占用,Task2 將直接獲取資源。 如果信號量被 Task1 占用,任務 Task2 將由運行態轉到阻塞狀態,
等待資源可用。一旦獲取了資源並使用完畢后會通過函數 xSemaphoreGive 釋放掉資源。
FreeRTOS 中斷方式計數信號量的實現
FreeRTOS 中斷方式信號量的實現是指中斷函數和 FreeRTOS 任務之間使用信號量。 信號量的中斷方
式主要是用於實現任務同步,與前面章節講解事件標志組中斷方式是一樣的。
下面我們通過如下的框圖來說明一下 FreeRTOS 中斷方式信號量的實現,讓大家有一個形象的認識。

運行條件:
創建一個任務 Task1 和一個串口接收中斷。
信號量的初始值為 0,串口中斷調用函數 xSemaphoreGiveFromISR 釋放信號量,任務 Task1 調用
函數 xSemaphoreTake 獲取信號量資源。
運行過程描述如下:
任務 Task1 運行過程中調用函數 xSemaphoreTake,由於信號量的初始值是 0,沒有信號量資源可
用,任務 Task1 由運行態進入到阻塞態。
Task1 阻塞的情況下,串口接收到數據進入到了串口中斷服務程序,在串口中斷服務程序中調用函數
xSemaphoreGiveFromISR 釋放信號量資源,信號量數值加 1,此時信號量計數值為 1,任務 Task1
由阻塞態進入到就緒態,在調度器的作用下由就緒態又進入到運行態,任務 Task1 獲得信號量后,信
號量數值減 1,此時信號量計數值又變成了 0。
再次循環執行時,任務 Task1 調用函數 xSemaphoreTake 由於沒有資源可用再次進入到阻塞態,等
待串口釋放信號量資源,如此往復循環。
上面就是一個簡單的 FreeRTOS 中斷方式信號量同步過程。 實際應用中,中斷方式的消息機制要注意以下
四個問題:
中斷函數的執行時間越短越好,防止其它低於這個中斷優先級的異常不能得到及時響應。
實際應用中,建議不要在中斷中實現消息處理,用戶可以在中斷服務程序里面發送消息通知任務,在
任務中實現消息處理,這樣可以有效地保證中斷服務程序的實時響應。同時此任務也需要設置為高優
先級,以便退出中斷函數后任務可以得到及時執行。
中斷服務程序中一定要調用專用於中斷的信號量設置函數,即以 FromISR 結尾的函數。
在操作系統中實現中斷服務程序與裸機編程的區別。
如果 FreeRTOS 工程的中斷函數中沒有調用 FreeRTOS 的信號量 API 函數,與裸機編程是一樣
的。
如果 FreeRTOS 工程的中斷函數中調用了 FreeRTOS 的信號量 API 函數,退出的時候要檢測是
否有高優先級任務就緒,如果有就緒的,需要在退出中斷后進行任務切換,這點與裸機編程稍有
區別,詳見 實驗例程說明(中斷方式):
另外強烈推薦用戶將 Cortex-M3 內核的 STM32F103 和 Cortex-M4 內核的 STM32F407,F429
的 NVIC 優先級分組設置為 4,即:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);這樣中
斷優先級的管理將非常方便。
用戶要在 FreeRTOS 多任務開啟前就設置好優先級分組,一旦設置好切記不可再修改。
計數信號量 API 函數
使用如下 18 個函數可以實現 FreeRTOS 的信號量(含計數信號量,二值信號量和互斥信號):
xSemaphoreCreateBinary()
xSemaphoreCreateBinaryStatic()
vSemaphoreCreateBinary()
xSemaphoreCreateCounting()
xSemaphoreCreateCountingStatic()
xSemaphoreCreateMutex()
xSemaphoreCreateMutexStatic()
xSem'CreateRecursiveMutex()
xSem'CreateRecursiveMutexStatic()
vSemaphoreDelete()
xSemaphoreGetMutexHolder()
uxSemaphoreGetCount()
xSemaphoreTake()
xSemaphoreTakeFromISR()
xSemaphoreTakeRecursive()
xSemaphoreGive()
xSemaphoreGiveRecursive()
xSemaphoreGiveFromISR()
關於這 18 個函數的講解及其使用方法可以看 FreeRTOS 在線版手冊 。
這里我們重點的說以下 4 個函數:
xSemaphoreCreateCounting ()
xSemaphoreGive ()
xSemaphoreGiveFromISR ()
xSemaphoreTake ()
因為本章節配套的例子使用的是這 4 個函數。
函數 xSemaphoreCreateCounting 
函數描述:
函數 xSemaphoreCreateCounting 用於創建計數信號量。
第 1 個參數是設置此計數信號量支持的最大計數值。
第 2 個參數是設置計數信號量的初始值。
返回值,如果創建成功會返回消息隊列的句柄,如果由於 FreeRTOSConfig.h 文件中 heap 大小不足,
無法為此消息隊列提供所需的空間會返回 NULL。
使用這個函數要注意以下問題:
1. 此函數是基函數 xQueueCreateCountingSemaphore 實現的:
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) \
xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
函數 xQueueCreateCountingSemaphore 的實現是基於消息隊列函數 xQueueGenericCreate 實現
的。
2. 使用此函數要在 FreeRTOSConfig.h 文件中使能宏定義:
#define configUSE_COUNTING_SEMAPHORES 1

函數 xSemaphoreGive

函數描述:
函數 xSemaphoreGive 用於在任務代碼中釋放信號量。
第 1 個參數是信號量句柄。
返回值,如果信號量釋放成功返回 pdTRUE,否則返回 pdFALSE,因為計數信號量的實現是基於消
息隊列,返回失敗的主要原因是消息隊列已經滿了。
使用這個函數要注意以下問題:
1. 此函數是基於消息隊列函數 xQueueGenericSend 實現的:
#define xSemaphoreGive( xSemaphore ) \
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, \
queueSEND_TO_BACK )
2. 此函數是用於任務代碼中調用的,故不可以在中斷服務程序中調用此函數,中斷服務程序中使用的是
xSemaphoreGiveFromISR。
3. 使用此函數前,一定要保證用函數 xSemaphoreCreateBinary(), xSemaphoreCreateMutex() 或者
xSemaphoreCreateCounting()創建了信號量。
4. 此函數不支持使用 xSemaphoreCreateRecursiveMutex()創建的信號量。
函數 xSemaphoreGiveFromISR

函數描述:
函數 xSemaphoreGiveFromISR 用於中斷服務程序中釋放信號量。
第 1 個參數是信號量句柄。
第 2 個參數用於保存是否有高優先級任務准備就緒。如果函數執行完畢后,此參數的數值是 pdTRUE,
說明有高優先級任務要執行,否則沒有。
返回值,如果信號量釋放成功返回 pdTRUE,否則返回 errQUEUE_FULL。
使用這個函數要注意以下問題:
1. 此函數是基於消息隊列函數 xQueueGiveFromISR 實現的:
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) \
xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )
2. 此函數是用於中斷服務程序中調用的,故不可以任務代碼中調用此函數,任務代碼中中使用的是
xSemaphoreGive。
3. 使用此函數前,一定要保證用函數 xSemaphoreCreateBinary()或者 xSemaphoreCreateCounting()
創建了信號量。
4. 此函數不支持使用 xSemaphoreCreateMutex ()創建的信號量


函數 xSemaphoreTake

函數描述:
函數 xSemaphoreTake 用於在任務代碼中獲取信號量。
第 1 個參數是信號量句柄。
第 2 個參數是沒有信號量可用時,等待信號量可用的最大等待時間,單位系統時鍾節拍。
返回值,如果創建成功會獲取信號量返回 pdTRUE,否則返回 pdFALSE。
使用這個函數要注意以下問題:
1. 此函數是用於任務代碼中調用的,故不可以在中斷服務程序中調用此函數,中斷服務程序使用的是
xSemaphoreTakeFromISR。
2. 如果消息隊列為空且第 2 個參數為 0,那么此函數會立即返回。
3. 如果用戶將 FreeRTOSConfig.h 文件中的宏定義 INCLUDE_vTaskSuspend 配置為 1 且第 2 個參數配
置為 portMAX_DELAY,那么此函數會永久等待直到信號量可用。
實驗例程現象(任務間通信)

FreeRTOS 實驗_計數信號量(中斷方式)——及其重要
K2 鍵按下,啟動單次定時器中斷,50ms 后在定時器中斷給任務 vTaskBeep 發送同步信號。 
這個中斷程序調試了我大半下午,現在總結一下要點:
我的定時器中斷服務函數:
void BASIC_TIM_IRQHandler (void) { if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET ) { ulHighFrequencyTimerTicks++; cc++; TIM_ClearITPendingBit(BASIC_TIM , TIM_IT_Update); } if(cc==10000) { cc=0; TIM_ClearITPendingBit(BASIC_TIM , TIM_IT_Update); BaseType_t xHigherPriorityTaskWoken = pdFALSE; LED3_TOGGLE /* 發送同步信號 */ xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken); /* 如果xHigherPriorityTaskWoken = pdTRUE,那么退出中斷后切到當前最高優先級任務執行 */ portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }
portYIELD_FROM_ISR函數的參數,如果為pdTRUE,退出中斷就會切換到高優先級任務執行。那是因為這個portYIELD_FROM_ISR函數在參數為pdTRUE時,會調用portYIELD()函數強制上下文切換。
FreeRTOS有兩種方法觸發任務切換:
- 執行系統調用,比如普通任務可以使用taskYIELD()強制任務切換,中斷服務程序中使用portYIELD_FROM_ISR()強制任務切換;
- 系統節拍時鍾中斷
對於Cortex-M3,M4平台,這兩種方法的實質是一樣的,都會使能一個PendSV中斷,在PendSV中斷服務程序中,找到最高優先級的就緒任務,然后讓這個任務獲得CPU運行權,從而完成任務切換。
對於第一種任務切換方法,不管是使用taskYIELD()還是portYIELD_FROM_ISR(),最終都會執行宏portYIELD(),代碼如下:

從上面的代碼中可以看出,PendSV中斷的產生是通過代碼:portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT實現的,它向中斷狀態寄存器bit28位寫入1,將PendSV中斷設置為掛起狀態,等到優先級高於PendSV的中斷執行完成后,PendSV中斷服務程序將被執行,進行任務切換工作。
PendSV中斷服務程序源碼是一段匯編,我們這里不去深究,畫出這段匯編程序的流程圖:

(此處摘自:http://blog.csdn.net/zhzht19861011/article/details/51418383)
我們只用知道這個中斷函數會進入臨界區和退出臨界區就行了,這也是我剛開始做實驗找到了影響實驗的因素卻沒有找到源頭,經過上面的流程圖就豁然開朗了。
要進入臨界區,就要關閉優先級大於 等於configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY這個宏配置的值的所有中斷,這個值我之前配置成3,而我的定時器中斷優先級配置成2,造成的實驗現象就是,定時器中斷進入一次之后,系統就卡死了。從邏輯上講,這樣設置是可以的才對的啊,我不想要os管理我的定時器中斷,所以就不屏蔽我的定時器中斷,但是FreeRTOS有自己的機制和解釋,我們需要遵守別人的規定,繼續看下面。卡在這里:

一直在尋找中斷優先級
當我把定時器中斷的優先級的數學字面值設置成大於等於configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY這個值時,系統就正常運行了。
這也就是說,在我們的實驗中,必須保證定時器中斷被freertos管理,畢竟我們的發送同步消息函數是在定時器中斷函數中的,也就是我們定時中斷設置的優先級,在數學字面值(后來看官方文檔,這個數學字面值既然巧合的默契了)上,應該大於等於configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY宏的值,這樣系統才可以正常運行。
上面這一段是我自己在沒有看到官方解釋時候的理解,果不其然,在卡死函數下方,官方給出了參考鏈接。現在截取關鍵部分展示:

譯:
數字優先級值與邏輯優先級設置之間的反向關系
Cortex-M硬件細節
接下來要知道的是,在ARM Cortex-M內核中,數字低優先級值用於指定邏輯高的中斷優先級。例如,分配數值優先級值為2的中斷的邏輯優先級高於分配數字優先級為5的中斷的邏輯優先級。換句話說,即使數字2較低,中斷優先級2高於中斷優先級5.希望能夠清楚:分配數字優先級為2的中斷可以中斷(嵌套)一個分配數字優先級為5的中斷,但是分配數字優先級為5的中斷不能中斷分配了數字中斷優先級為2的。
這是ARM Cortex-M中斷優先級的最直觀的方面,因為它與大多數非ARM Cortex-M3微控制器架構相反。
使用RTOS時的相關性
以“FromISR”結尾的FreeRTOS功能是中斷安全的,但是即使這些函數的邏輯優先級高於configMAX_SYSCALL_INTERRUPT_PRIORITY(configMAX_SYSCALL_INTERRUPT_PRIORITY在FreeRTOSConfig.h頭文件中定義)的優先級,也不能調用這些函數。因此,使用RTOS API函數的任何中斷服務程序必須將其優先級手動設置為數值等於或大於configMAX_SYSCALL_INTERRUPT_PRIORITY設置的值。這確保中斷的邏輯優先級等於或小於configMAX_SYSCALL_INTERRUPT_PRIORITY設置。(果然和我想的一樣,這也就是既然你調用了FreeRTOS的API,你的中斷優先級設置就應該是屬於FreeRTOS管理的)
Cortex-M中斷優先級的默認值為零。零是可能的最高優先級值。因此,不要將使用中斷安全RTOS API的中斷優先級設置為其默認值。
這也就是說,官方都已告訴我們了,我們既然使用別人的產品,就要遵守別人的規定和約束,終於,解惑了。
