2011年9月27日 星期二

Serial驅動程式中的Flow Control問題

什麼是Flow Control
Flow Control簡單講就是流量控制,其主要目的就是避免通訊的雙方資料被掉包,那為何資料會掉包呢?我們想像一個情境,通訊一定是至少兩人以上互相傳送資料,互相傳送資料雙方硬體不一定會一樣,所以在效能上也會不一樣,若當接收端來不及處理已接收到資料時,若沒有通知對方暫停傳送資料,因而造成對方持續送資料,這樣就會產一種現象,就是接收端會將收進來的資料丟棄,或者不接收對方送過來的資料,因而造成資料的留失,所以我們就使用流量控制的部分來確保資料不要留失。其實其中的做法也不是太難,就是利用一個機制來通知對方不要送資料就好了,而問題在於以前這種的控制都由軟體來做,但是最後還是控制使用硬體來通知對方,在控制硬體送出訊號給對方會需要一些時間處理,而這段時間長短是無法預測的,並且對方還是在繼續傳送資料,假如這段時間過長,就會造成和没有流量控制一樣的情形,也因此為了解決這個問題,現在就有人改用硬體來處理這個問題,就可以大大降低這個問題的產生,但是若要真正有效發揮硬體的功能是需要修改軟體的,所以以下我們會來說明如何修改軟體。
Serial Port的流量控制方式
RS232的流量控制方式有兩種,一種稱為軟體流量控制方式,另外一種就是所謂的硬體流量方式,什麼是軟體流量控制方式,其控制方式就是將控制訊息加到資料之中,用另外一個方式說明,就是當接收端無法收資料時就送一個字元Xoff (0x13)給對方,可以收的時候就送一個字元Xon (0x11)給對方,所以軟體控制的方式有幾種限制,首先是在傳送的資料不可以包含這兩個字完,否則將會嚴重破壞流量控制的正常運行,另外一個限制就這兩個字元必須是要即時被送出,若和要送給對方的資料一起排隊的話,有可能會延誤流量控制的時間點,因而造成流量控制的失敗。
另外一種的流量控制方式為硬體方式,其作法是必需另外再接兩條線,一條為RTS至對方的CTS,因為是雙向的所以另外一條就是我的CTS接至對方的RTS,另外的說法雙方都會增加兩隻腳為RTS及CTS,其做法很簡單就是當接收端資料快滿的時候,將RTS設為low,一直到可以接收資料時再將RTS拉為high,相對的傳送端會隨時檢查CTS,當它為low時就停止送資料,當它為high時就開始送資料。
RS232的上層控制驅動程式為tty_io,而這層則會負責流量控制,當需要做相對流量控制的行為時,它會呼叫你的RS232驅動程式作動作,所以只要做相對的動作即可,而這個動作則是前一篇文章-serial port device driver中所提的throttle & unthrottle這兩個call back function。
有什麼問題嗎?
若按照前面所提的,在寫串列式驅動程式的時候有什麼問題?看是沒有問題,但問題就在後面,因為現在的UART越做越好,所以就將流量控制的動作直接由IC本身實現,就不需要軟體來處理,IC實現有它的好處,因為它是由硬體來處理,所以反應的時間會非常快,能夠避免軟體處理時需要冗長的時間,可能會造成反應不及而讓流量控制無效。在找出問題之前我們先看以下的圖:

以上為驅動程式的架構圖,通常每一層驅程式都會有自已的buffer來存放收送的資料,而前面有說過在tty_io層會做流量控制,而它是如何判斷何時需要做流量控制呢?當然是依照它自已的buffer來做判斷,快滿的時候就叫對方不要送資料,buffer已被讀完則通知對方可以送資料,問題在於當上層的buffer滿並不代表低層IC上的buffer已滿,如此一來有一種情境將會發生,底層的IC buffer已滿,因為有支援流量控制,所以IC已經主動通知對方不要送資料,可是這時候因為上層的資料未滿,或者剛到低水位,所以通知低層送出通知對方可以送資料,如此一來將會弄亂底層的流量控制,簡單的說明就是上層的buffer水位和底層是不會同步的,用另外一種方式來看待這個問題,其實這麼多層的驅動程式,只要其中有一層做流量控制即可,這時候當然讓最低層的IC來處理即可,因為可以反應最快,並且因為由IC自行處理,所以可以減輕CPU的負擔,所以在有IC支援流量控制時RS232的驅動程式,應該如以下的寫法將會是比較好的:
static void uart_throttle(struct tty_struct *tty)
{
…………
disable_IC_receiving_and_receiving_interrupt();
………………..
}
static void uart_unthrottle(struct tty_struct *tty)
{
………………
enable_IC_receiving_and_receiving_interrupt();
…………………
}
没錯一個簡單的動作,就是在throttle的callback function中停止收資料就好了,因為如此一來將會造成IC buffer到達高水位而讓IC自動作成流量控制的行為即可,另外unthrottle的callback function中再一次啓動收資料即可,因為將IC中的buffer讀走而達低水位時,IC將會再次自動做出流量控制,如此一來上層可以不用知道低層IC的buffer使用情形,也不會弄亂IC的流量控制,這是在IC支援流量控制時所要做的修正。

2011年9月12日 星期一

驅動程式中的DMA問題

前言
什麼是DMA(directly memory access)? 簡單的講就直接記憶體存取,或許你還是無法明白,這樣說好了,在一個電腦系統中,通常都是由CPU (中央控制單元) 來對記憶體做存取的動作,也就是在各個電腦週邊元件中,若要對記憶體做存取動作,則必須經由 CPU 來做仲介的角色,而DMA則可以取代CPU這個工作,但僅至於資料而已無法對指令做這個動作,因為記憶體在系統中是唯一的,若要讓CPU及DMA可以很順暢的工作而不會互相干擾,則必需在軟體上做一些協調的動作,另外因為CPU有MMU及Cache的元件,也使得為了要讓DMA取得正確的資料,所以在寫這方面的驅動程式時需要特別注意,以下就來說明這方面的作法及問題。
硬體架構
圖一
問題點
從上圖中我們可以清楚地了解以下幾件事:
  • 當CPU要向DRAM或者是週邊元件存取時,所以放出來的位置將會經過cache及MMU的處理。
    • 在cache中的處理主要是提高對記憶體的存取速度。
    • 當作業啓動時所有程式均使用虛擬記憶體,所以一定會經過MMU的位址轉換,因為當實際要對週邊元件或者記憶體存取時一定會使用實體位址。
  • 當DMA要存取記憶體時並不會經過MMU模組的轉換,換句話說就是DMA會直接使用實體位址來存取記憶體。
    • 另外一個問題是DMA也沒有經過cache模組,所以存取的資直接從記憶體存取。
解決方案
由以上的問題描述,我們可以看一個很基本的問題,就是CPU和DMA之間對記憶體的存取可能會有不相符的現象產生。首先我們先來看週邊元件要送資料,也就是從CPU寫入資料至記憶體,然後DMA從記憶體取得資料給週邊元件,進而週邊元件送出資料,這裡問題在於CPU寫入資料時,可能不會一定寫入記憶體,有可能保留至cache中,若這時DMA來取資料將會是不對的資料,因此在DMA存取之前,一定要讓CPU的cache單元做一次sync的動作,將若有還有cache中的資料,真的寫入記憶體中,如此一來DMA將可以取得正確資料。換一個簡單的說法,就是週邊元件要送資料時必須對cache做sync的動作,確認最後的資料是有寫入記憶體。
另一個資料方向就是當週邊元件收到資料時,將會由DMA直接寫入記憶體,完作接收後,週邊元件會產生一個中斷通知CPU來讀取資料,這時的問題是在於存放收到資料,有可能是已被cache住,若CPU不做任何動作而直接來讀取資料,一樣會產生資料不正確的情形,這時在CPU讀取時必須對該區塊作一個cache invalid的動作,讓CPU在讀取該區塊資料,會實際至記憶體做讀取的動作,因此重新讓cache元件,針對該區域重新做cache的動作,如此一來就不會有資料不正確的情形產生。所以說週邊元件在收資料時,必需對cache做invalid的動作。
另外有一個簡單的作法,就是在準備這些記憶體時,就取得讓記憶體是不會做cache的動作,如此一來就不需要做資料同步動作。
Kernel API
我們從以上知道問題,也知道該如何解決,現在的問題是有多少的kernel API可供使用,以下我們就來看:
dma_addr_t dma_map_single(struct device *, void *, size_t, enum dma_data_direction);
void dma_unmap_single(struct device *, dma_addr_t, size_t, enum dma_data_direction);
dma_addr_t dma_map_page(struct device *, struct page *,unsigned long, size_t, enum dma_data_direction);
void dma_unmap_page(struct device *, dma_addr_t, size_t,enum dma_data_direction);
void dma_cache_maint(const void *kaddr, size_t size, int rw);
void dma_cache_maint_page(struct page *page, unsigned long offset,size_t size, int rw);
enum dma_data_direction {
        DMA_BIDIRECTIONAL = 0,
        DMA_TO_DEVICE = 1,
        DMA_FROM_DEVICE = 2,
        DMA_NONE = 3,
};
以上的kernel API主要是針對將一般取得的記憶體(例如由kmalloc所取得),是會被cache的記憶體,將cache中的資料和實體記憶體資料做同步的動作,其DMA_TO_DEVICE就是做invalidate的動作,而DMA_FROM_DEVICE就是synchronize的動作,這些API必需要在呼叫DMA之前完成。
void *dmam_alloc_coherent(struct device *dev, size_t size,dma_addr_t *dma_handle, gfp_t gfp);
void dmam_free_coherent(struct device *dev, size_t size, void *vaddr,dma_addr_t dma_handle); 
以上的kernel API是在要allocate記憶體時就取得不會被cache的記憶體,如此一來對這塊記憶體的存取時就不需要前面所提的動作 – invalidate or synchronize,另外也可取得該記憶體的實體記憶體,這個實體記憶體將會給DMA controller所使用。