1. 程式人生 > >環形佇列,動態記憶體

環形佇列,動態記憶體

STM32 串列埠驅動(拼音檢索測試通過)(環形佇列+記憶體動態分配+DMA)

在設計串列埠驅動的過程中,要遵循的兩條準則是:
1:儘量的減少程式執行的時間。
2:儘量的減少程式所佔用的記憶體。
譬如,下面的一段程式:
程式段 1-1
/指標是指向 ptr,需要傳送 count個數據/
void USART1WriteDataToBuffer(*ptr,u8 count)
{
/判斷資料是否傳送完畢/
while(count–)
{
/傳送資料/
USART1SendByte(*ptr++);
/等待這個資料傳送完畢,然後進入下一個資料的傳送過程

/
while(USART_GetFlagStatus(USART1,USART_FLAG_TC);
}
/資料傳送完畢,返回/
}
很明顯,這段程式在實際應用中將會產生災難性的後果,首先,當傳送資料送到傳送暫存器
啟動傳送以後,CPU就一直在等待這個資料傳送完成,然後進入下一個資料的傳送,這樣,
直到所有要傳送的資料完成,CPU才能做其他的事情。相對於 CPU核心執行的速度而言,串
口外設的執行速度是非常快的,讓一個速度非常快的裝置去等待相對很慢的裝置,程式的效
率是非常低下的。

所以必須採用中斷的方式傳送資料。
程式段 1-2
/將資料寫入傳送緩衝區

/
void USART1WriteDataToBuffer(*ptr,u8 count)
{
while(count!=’\0’)
{
USART1SendTCB[Index++]=*ptr++;
Count=count;
}
/……判斷溢位等其他程式碼省略…../
}
/……傳送中斷的 ISR…../
void USART1SendUpdate(void)
{
/……判斷髮送緩衝區中的資料是否傳送完畢…../
/將傳送緩衝區的資料傳送出去/
USART1SendByte(*ptr++);
/……傳送指標加一,待發送的位元組數減一等程式碼…../
}
這樣,當呼叫 USART1WriteDataToBuffer函式將待發送的資料寫入傳送緩衝區以後,CP
U就可以執行其他的任務,待一個數據傳送完成以後,中斷 ISR就會觸發,在中斷服務程式
裡面將下一個資料寫入傳送暫存器,啟動下一次傳送,知道完全傳送完畢為止。
很明顯,上述的程式的設計比較好,不用佔用過多的 CPU時間。
在實際的工程應用中,經常會出現類似這種情況:串列埠顯示屏需要顯示 1000個點,通過
串列埠傳送這 1000個點的顏色的 RGB亮度值。將這 1000個數據寫入傳送緩衝區以後,啟動發
送。在 115200的波特率,一位起始位,一位停止位,無校驗位的情況下,至少需要(10*10
00*2)/115200=0.1736秒,在這期間以內,時鐘更新了,需要再發送給串列埠一串時間更新的
資料,這個資料大約有 100個,這樣這串資料需要寫入到傳送緩衝區的傳送位元組的後面。
同樣道理,在這個時候如果有顯示任務更新的話,將會有其他的資料寫入到傳送緩衝區。

串列埠 1

從圖上可以看出,程式段 1-2雖然滿足了時間上的要求,卻沒有滿足空間上的要求,它

的資料緩衝區是單向的,這樣,當傳送緩衝區的所有的資料全部發送完畢後,或者當傳送緩
衝區撐滿了以後才能將傳送緩衝區內的資料清空,以便裝入下次的緩衝資料。這樣記憶體較小
的嵌入式系統來說是不能容忍的。
因此,可以將傳送緩衝區建立成一個環形的緩衝區,在這個環形緩衝區內,通過頭指標(Hos
tIndex)和尾指標(HostIndex)來定位空白區和資料區。
(1):頭指標(HostIndex)指向有資料區的頂部,每次寫入資料,都更新頭指標,如果到了
緩衝區的末端(EndIndex),就自動返回到緩衝區的起始處(StartIndex),直到寫入到尾指標
處為止,這時緩衝區已經被裝滿,不能再裝入資料。
(2):尾指(TailIndex)針指向有資料區的尾部,當資料傳送完畢後,更新尾指標的位置,
如果到了緩衝區的末端(EndIndex),就自動返回到緩衝區的起始處(StartIndex),直到遇到
頭指標為止,這是證明所有的資料已經發送完畢。

串列埠 2
這樣就實現了傳送緩衝區的動態調整空白區和資料區,剛剛傳送完畢的資料,馬上就被開
闢出來用於存放下個數據,最大可能的節省了寶貴的傳送緩衝區的空間,提高了使用效率。
這個程式比較複雜,大致的流程如下(省略了狀態的判定,保護措施等程式碼)
程式段 1-3
/將資料寫入傳送緩衝區/
void USART1WriteDataToBuffer(*ptr,u8 count)
{
while(count!=’\0’)
{
/頭指標不等於尾指標,緩衝區沒有撐滿/
if(USART1HosIndext!=USART1TailIndex){
USART1SendTCB[USART1HosIndex]=*ptr++;
/更新頭指標,如果到了緩衝區的末端,就自動返回到緩衝區的起始處/
if(++USART1HosIndext>=USART1_SEND_MAX_BOX)USART1HosIndext=0;}
}
/……判斷溢位等其他程式碼省略…../
}
/……傳送中斷的 ISR…../
void USART1SendUpdate(void)
{
/頭指標不等於尾指標,緩衝區尚有未發生完的資料/
if(USART1HosIndext!=USART1TailIndex){
/將傳送緩衝區的資料傳送出去/
USART1SendByte(*USART1TailIndex);
/更新尾指標的位置,如果到了緩衝區的末端,就自動返回到緩衝區的起始處/
if(++USART1TailIndex>=USART1_SEND_MAX_BOX)USART1TailIndex=0;
/……判斷溢位等其他程式碼省略…../
}
值得注意的是,一些微控制器中,例如在 Cortex-M3的微控制器架構中,有 DMA傳送模
式,可以配置一個內部的通道,將指定的地址處的資料,在無須 CPU的管理下,直接將其發
送到串列埠傳送暫存器裡去。通過這個方法,可以大大的降低了傳送過程中重複進入中斷的次
數,從而大大提高了效率。這樣,如果使用了這個晶片,就可以使用 DMA模式進行傳送。但
是 DMA傳送模式下,對於頭指標和尾指標就得做出一些修改,因為 DMA傳送過程中,是不能
讓頭指標到達緩衝區終點後,自動將指標調整到起點位置的。
但是,加入傳送管理結構體以後,上述問題可以得到解決。
利用記憶體塊動態分配可以大大減少提高記憶體的使用效率,尤其是對於串列埠通訊而言,更
是如此。利用記憶體管理模組可以將微控制器除全域性變數和靜態結構變數以外的剩餘的記憶體統
一管理,在需要時候申請,在不用的時候釋放,如串列埠的傳送緩衝區,乙太網,SD卡,外部
資料儲存器等等均可以用記憶體來管理,可以重複使用,大大提高了使用效率。
記憶體分配 1 (原檔名:記憶體分配 1.JPG)

首先定義傳送緩衝區管理塊的結構體
typedef struct{
unsigned char Num; //該儲存區儲存的有效位元組數量
unsigned char *Index; //該儲存區申請的記憶體塊的指標
unsigned char *MemIndex; //該儲存區申請的記憶體塊管理區的指標
}USART1SendTcb;
例如需要 200位元組:

串列埠 3

這樣,加入動態記憶體與傳送緩衝區管理塊以後,無論是採用 DAM模式傳送資料還是普通

的方式,都可以輕易的配置。將記憶體塊的指標值 Index傳給 DMA的傳送地址,將待發送的字
節數 Num傳給 DMA的傳送位元組計數暫存器,就可以完成無須 CPU管理的操作,最大的減少了
CPU的使用,大大提高了記憶體效率。或者採用普通的中斷模式。
具體的程式碼較長,見工程檔案,不再詳細列出。

      記憶體管理  
在嵌入式裝置中,往往會存在一些任務需要大量的記憶體,在記憶體相對較少的微控制器中,

怎樣有效管理這些寶貴的資源,是必須解決的一個重要問題。
在上位機的程式設計中,我們通常使用 malloc()函式以及 Free()函式來完成對記憶體的管
理。這是因為相對嵌入式系統而言,上位機的記憶體非常大,而且 Windows提供了很好的記憶體
管理介面,所以不存在問題。但是在嵌入式系統中,大量使用上述函式會出現兩個問題:
(1)產生記憶體碎片的問題。
在執行的過程中,各個任務頻繁的呼叫記憶體分配和釋放,會導致原本一整塊空間地址連
續的區域分散成一堆實體地址上相互獨立的區域,這樣有可能導致一個程式需要一個較大的
記憶體,空餘的記憶體塊沒有一個連續的地址,無法分配給任務。久而久之,最後系統可能連一
個很小的實體地址都分配不到,最後導致系統的崩潰。
如下圖所示:

記憶體分配 2

在上圖中可以看到,雖然起始地址為 20000的記憶體區有 16個空白的位元組,但是仍然無法

為任務分配到四個位元組的實體記憶體。
(2)執行的時間不確定的問題
在 free()函式中,存在著一些記憶體合併等功能,例如將釋放完成以後,將空間上相近
的兩個空白區域合併為同一個,將存在記憶體碎片的區域重新整合,甚至可能使用了二叉樹等
非線性資料結構,等等操作。
而這些函式所耗費的時間是無法確定的,在實際的應用中,對於記憶體這種全域性變數,多
個任務都要用到,為避免會存在可重入性的問題,必須採用訊號同步的方法,或者暫時關閉
中斷的方法,來同步對各個任務對共享資源的使用。這樣,導致了系統死區時間的增加,響
應速度的變慢,不確定性增加。
因此,在大多數嵌入式系統中,通常採用靜態記憶體塊池的方法。將系統空餘的記憶體統一
管理,生成一系列的大小固定的記憶體塊池,在實際的操作中,以這一整個記憶體塊進行操作。
實現過程
首先定義記憶體管理塊的結構體
typedef struct OSMEMTCB{
void *OSMemFreeList;//用於指向該管理區中的空白的記憶體塊
u8 OSMemBlkSize;//用於該管理區中的每個記憶體塊的位元組數
u8 OSMemNBlks;//用於該管理區中的分為多少個記憶體塊
u8 OSMemFreeNBlks;//用於該管理區還剩多少空白記憶體塊

}OSMEMTcb;
將一個靜態的儲存區分配給記憶體配置函式,記憶體管理塊的各個列表的含義如下圖所示:

記憶體分配 3

每個記憶體塊的頭四個位元組用於儲存下一個記憶體塊的指標地址,直到倒數第一個為止,最

後一個指標指向一個空的指標,表明已經到達記憶體區的的末端。
在實際運用過程中,OSMemFreeList是指向空白的記憶體塊的指標,通過它來申請記憶體,
當申請到記憶體塊以後,OSMemFreeList指向當前資料塊的下一個記憶體塊節點地址(記憶體管理
函式已經自動將所有記憶體塊通過指標連結成一個單向連結串列),當釋放記憶體塊的時候,將 OSMe
mFreeList指向當前釋放的記憶體塊,將當前記憶體塊的下一個記憶體塊指標指向先前的 OSMemFre
eList。OSMemFreeNBlks儲存著該記憶體區空白塊的數量,若記憶體塊已滿,返回錯誤程式碼,OSM
emBlkSize指的是每個記憶體塊內位元組數量,它的大小可以根據需要指定,理論上是它越小,
內塊的利用率就越高,例如儲存一個 101個位元組的資料,若一個記憶體塊的大小是 10個位元組,
則需要 11個記憶體塊,若一個記憶體塊的大小是 100個位元組,則需要 2個記憶體塊,最後一個記憶體
塊僅僅使用了一個位元組。但並非記憶體塊的越小越好,因為儲存下個記憶體塊節點的地址需要 4
個地址位,記憶體塊越小,儲存地址的資料所佔比例越高。在實際操作 32位元組過程中,可以定
義大小不同的記憶體塊,靈活運用。

記憶體分配 4

記憶體配置函式的核心程式碼:OSMemCreate(……)

記憶體分配 5
for(i=0;i