2019年2月17日 星期日

SPI to Ethernet Driver for RTOS

去年因為工作上常常接觸到 lwIP,所以只要一有空就啃"嵌入式網路那些事",啃了一整年終於啃到第 12 章-操作系統模擬層。

但第 12 章的中斷範例太過簡單粗暴,完全沒有用到 RTOS 的優點,原本就反對使用 RTOS 的人說不定還會嘲笑這種設計。

趁著這次春節連假,筆者把範例改寫為更貼近業界、更具工業強度,終於在初五完工。

就算你沒有這本書,如果你想知道為何要用 RTOS、什麼才是 RTOS 的正確用法,或者你想吐槽筆者,本篇都值得一讀。

本篇文章不講的東西

筆者的習慣是如果很多人寫過的東西就盡量不要再提,除非筆者認為能寫得比其他人更好或是有不一樣的觀點,下列項目已經很多參考資料,筆者就不再多做解釋:
  • MicroC/OS-II
  • SPI 
  • STM32
  • lwIP

本篇文章要講的東西

市售 MCU 書籍大多採取個別章節解釋個別元件的用法 - 用一種支離破碎的方式呈現。但在真實世界,每個元件是在一個「系統」中運行,就如亞里斯多德的名言「整體大於部份之和」,這時候絕對不是「會動就好」,除了工程上應該避免疊床架屋,從系統思考的角度來看,你在某部份的設計提供系統的是正回饋(positive feedback)還是負回饋(negative feedback)呢?

系統思考一書對負回饋的定義為「取消或抵銷改變」,正回饋相反,帶來更多改變,也就是「滾雪球效應」。

講具體一點,當網路通訊越頻繁,CPU loading 也越高(正回饋),所以你的設計不良 CPU loading 也會越高。本篇要探討的就是如何設計一個優良的 SPI to Ethernet Driver - 即使網路通訊頻繁,也不會消耗太多 CPU,更在適合的時間點釋放 CPU 使用權,降低 CPU loading(負回饋)。

書中範例的缺點

很多人手上沒有書,這裡還是要稍微提一下這本書在講什麼,這本書主要講述如何把 lwIP (A Lightweight TCP/IP stack) 移植到 STM32,但 STM32 本身並沒有 Ethernet Controller,所以外掛一顆 ENC28J60(Stand-Alone Ethernet Controller with SPI Interface)。

lwIP 支援 Bare Metal(無 OS 環境) 與 RTOS 兩種運作模式,書中第 12 章就是講述如何把 lwIP 移植到 RTOS(MicroC/OS-II)上。

為何筆者稱範例「簡單粗暴」?如下圖所示,當 ENC28J60 收到 Ethernet frame,會輸出中斷訊號給 STM32:


問題在於範例在 ISR 內直接用 CPU polling 的方式把 Ethernet frame 從 SPI interface 一個 byte 一個 byte 搬回來,雖然 ENC28J60 SPI clock 最大可達 20MHz,但相較於 STM32 72MHz 還是有一段差距,而且傳送 MTU(1500bytes) 需要 0.6ms,這違反了 ISR 應盡量簡短、快進快出的原則。
//ISR
void EXTI1_IRQHandler(void)
{
    OS_CPU_SR  cpu_sr;

    OS_ENTER_CRITICAL();
    OSIntNesting++;
    OS_EXIT_CRITICAL();

    if(EXTI_GetITStatus(EXTI_Line1) != RESET)     
    {
        EXTI_ClearITPendingBit(EXTI_Line1);
        process_mac();  //polling SPI bus
    }

    OSIntExit();
}

改進1: CPU polling SPI 移出 handler mode

第一步把 ISR 內的 CPU polling SPI 從 handler mode 搬到 thread mode。ISR 用 semaphore 之類的 IPC 通知高優先權 task 去 polling SPI bus,而不是在 ISR 內 polling SPI bus:
//ISR
void EXTI1_IRQHandler(void)
{
    OS_CPU_SR  cpu_sr;

    OS_ENTER_CRITICAL();
    OSIntNesting++;
    OS_EXIT_CRITICAL();

    if(EXTI_GetITStatus(EXTI_Line1) != RESET)     
    {
        EXTI_ClearITPendingBit(EXTI_Line1);
        OSSemPost(EthernetRxSem); //signal semaphore
    }

    OSIntExit();
}

void SpiDriverTask(void *pData)
{
    //...
    while(1)
    {
        //...
        OSSemPend(EthernetRxSem, 0); //wait semaphore...
        process_mac(); //receive Ethernet frame from SPI bus
        //...
    }

}
此外,因為原來的範例中 Ethernet Tx 與 lwIP task 嵌合在一起,如果 Ethernet Tx 與 Ethernet Rx 放在不同的 task,就會發生兩個 task 爭奪 SPI bus 的情形,雖然可以用 mutex 上鎖,但這種設計還是讓人感到哪裡不對勁。

比較合理的設計是把 Ethernet Tx/Rx 放到一個獨立的 task,ISR 加上這個 task 就是 SPI to Ethernet driver:
  • 從分層的角度來看,剛好與 TCP/IP 4 層對應,結構清晰(圖 2)。
  • 賦予 SPI to Ethernet driver task 較高的優先權,當 SPI driver task 執行時不被其他 task 中斷。
  • STM32 SPI Master 支援 DMA,當 SPI to Ethernet driver task 使用 DMA 傳輸資料時,可以把 CPU 使用權釋放給其他 task (如 lwIP task, App task)
  • 如果把 SPI 與 lwIP 合體,lwIP 不支持這種作法,只能放在 lwIP timer,效率將其差無比。 
  

這個 SPI to Ethernet Driver Task,有可能同時收到 Tx(來自 lwIP) 與 Rx(來自 ISR)的要求,MicroC/OS-II 提供了 Event Flag API,所以可以把 Tx/Rx 分別對應到不同的 flag(bit),只要其中一個成立就代表要需要透過 SPI bus 收送 Ethernet frame,從下面的程式碼相信就能明白。
void TaskEth(void* p_arg)
{
    while(1)
    {
        INT8U os_err;
        OS_FLAGS flags;
        
        flags = OSFlagPend(EthFlags, 0xFFFF, (OS_FLAG_WAIT_SET_ANY|OS_FLAG_CONSUME), 0, &os_err);
        if(os_err != OS_ERR_NONE)            
            continue;
        
        if(flags & ETH_INPUT_FLAG)
            process_mac();
            
        if(flags & ETH_OUTPUT_FLAG)
            process_mac_output();
        
        if(flags & ETH_LOW_LEVEL_INIT_FLAG)
            process_low_level_init();
    }
}
如果 Tx/Rx 同時成立,那該先處理 Tx 還是 Rx? 因為 TCP 傳送 segment 的速度與接收 ACK 的速度有相依性,所以這邊先收再送。

剩下一個問題是 lwIP 填好的 TCP/IP 封包要怎麼交到 SPI to Ethernet Driver Task 手上? MicroC/OS-II 提供固定區塊大小的記憶體配置 API(甚至你也可以 DIY),可以以 1502 為區塊大小(MTU 1500bytes 加上 2bytes 描述長度),再配合 ring buffer,就能把欲傳送的 TCP/IP 封包丟給 SPI to Ethernet Driver Task 了,如圖 3 所示。


改進2: 使用 SPI DMA

 聰明的讀者應該會發現,雖然 lwIP task 與 SPI to Ethernet Driver task 間墊了一層 ring buffer,但因為 SPI to Ethernet Driver task 優先權較高,只要 SPI to Ethernet Task 被觸發 CPU 就會被搶走,接著從 ring buffer 取出 TCP/IP 封包開始傳送,一直到傳送完才有可能還給 lwIP task,還給 lwIP 時才有機會 push 下一筆 TCP/IP 封包。

由此可見,雖然有 ring buffer,但卻無法真正緩衝住什麼東西。有兩種方法可以改善這種問題。

1. 呼叫 OSTimeDly 釋出 CPU

這個方法的問題在於 OS tick 太粗會大幅削減 SPI 頻寬,太細則是 OS task switch overhead 會削減 RTOS 的優勢(比方說如果你把 tick 設定為 1us)。
unsigned char SPI1_ReadWrite(unsigned char writedat)
{
    /* Loop while DR register in not emplty */
    while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET);

    /* Send byte through the SPI1 peripheral */
    SPI_I2S_SendData(SPI1, writedat);

    /* Wait to receive a byte */
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET)
        OSTimeDly(1);   //release CPU to other task(s)

    /* Return the byte read from the SPI bus */
    return SPI_I2S_ReceiveData(SPI1);
}

2. DMA

方法 1 幾乎沒有實用性,想要叫 CPU 不介入,在 STM32 上唯一可行的方式就是 DMA。利用 DMA interrupt + Semaphore 就可以在 DMA 時把 CPU 還給 lwIP,這時候就有機會多傳幾個 TCP/IP 封包,頻寬無形之中增加了。

更重要的是 CPU loading 也減輕了,在前面提到負回饋的觀念,如果用 CPU polling SPI,當系統通訊的越頻繁,CPU 也越吃重(正回饋),利用 DMA搭配 Semaphore,就可以大幅減輕 CPU loading,在筆者的實際工作中,至少可以減少 20% 以上的 CPU loading!
void SPI1_DMA(uint32_t buf, uint32_t len, bool is_recv)
{
    INT8U err;
    
    SPI1_DMA_Config(buf, len, is_recv);
    SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx |  SPI_I2S_DMAReq_Tx, ENABLE);
#ifdef EN_SPI1_DMA
    OSSemPend(SPI1_DMA_Sem, 0, &err); //wait signal from ISR
#else
    while(DMA_GetFlagStatus(DMA1_FLAG_TC2)==RESET);
    while(DMA_GetFlagStatus(DMA1_FLAG_TC3)==RESET);
#endif    
}


void SPI1_DMA_Finish(void)
{
    OSSemPost(SPI1_DMA_Sem);
}

//ISR
void DMAChannel2_IRQHandler(void)
{
  OS_CPU_SR  cpu_sr;
  
  OS_ENTER_CRITICAL();
  OSIntNesting++;
  OS_EXIT_CRITICAL();
  if(DMA_GetITStatus(DMA1_IT_TC2) != RESET)     
  {
    DMA_ClearITPendingBit(DMA1_IT_TC2);
    DMA_ITConfig(DMA1_Channel2, DMA_IT_TC, DISABLE);
    DMA_Cmd(DMA1_Channel2, DISABLE);
    DMA_Cmd(DMA1_Channel3, DISABLE);
    SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx |  SPI_I2S_DMAReq_Tx, DISABLE);
    SPI1_DMA_Finish(); //signal semaphore   
  }
  
  OSIntExit();  
}

心得 

1x 年前小弟在某 HMI 公司服務時,當時的主管聲稱:「 UART 改用 DMA 也不會變快,因為HMI 傳送的資料長度每次都不固定...」

限於當時所學信以為真,多年後才知道這種說法不正確。DMA 主要目的不是用來加速,而是用來節省寶貴的 CPU。

以當時的時空背景,其實也很難有這種思維,因為當時 HMI 使用的 OS 純手工打造非常陽春。同事間也少有人懂如何評估 CPU Loading - 雖然 uC/OS-II 書上已經寫得很清楚了。再者,其實 UART 能消耗的 CPU 也很有限,直到筆者開始玩網路視訊,才知道 CPU 真的會被操爆。

由本篇文章可以知道,即使你有一個商用 RTOS 加上 CPU 支援 DMA...你也要知道如何正確組織程式才能發揮最大效益,否則 RTOS 反而可能成為你的絆腳石。

其實多年前 MicroC/OS-II 的作者就寫了一本好書 - Embedded Systems Building Blocks: Complete and Ready-To-Use Modules in C,詳細解釋如何在 RTOS 上正確使用硬體週邊,可惜的是這本書多年沒有更新,很多人可能不知道如何觸類旁通到其他的硬體週邊上(如本篇講的 SPI to Ethernet)。

要在網路通訊時減少 CPU loading,除了 DMA 外,TCP/UDP checksum 改用硬體計算也是一種方式,國內 Davicom(聯傑國際)ASIX(亞信) 等公司推出的 IC 上就具備這樣的功能,lwIP 也建議如果硬體支援最好改用硬體來做。甚至還有 TCP/IP 全盤硬體化的 IC,不過比較少見就是了。

最後要提一下前面提到的「系統思考」這本書,這本書會讓你跳出見樹不見林的思維,用一種整體的思維看待系統,如果你是那種覺得軟體慢就是應該換 CPU 把 RAM 加大的人,讀了這本書說不定會有不同的想法。

6 則留言:

  1. "趁著這次春節連假,筆者把範例改寫為更貼近業界...,終於在初五完工。"

    一般公司在春節加班,薪水是至少要 Double 的。

    但我知道你要說的是:這是給自己一個鍛鍊學習的機會。但最後你還是"免費的"

    用在公司給你的工作上,然後就慢慢的養成慣老闆的心態,付一樣的薪水可以

    "馴服"工程師的啦。自然就會造成中國人慣有的認為軟體是免費的觀念。

    https://www.ettoday.net/news/20190216/1379689.htm

    "抄課文惡夢掰了!「寫字機器人」年後狂銷 家長氣到直接砸爛"

    對啊~這一種多軸小型加工機周邊零組件都便宜到到處撿都有。

    但重點就是軟體免費的,淘寶網搜尋"寫字機器人"一堆。

    從小就無形中灌輸小孩子說:用這些東西,只要是買得到硬體,軟體就是免費的。

    也就是這一種錯誤的國民教育觀念惹惱了西方國家對於中國智慧產權的政策。

    我們這些會寫這一種看似"挑戰自己學習成長"軟體的工程師也是一個幫兇啊。

    因為就是我們平常工作就給該額外付費的老闆一個同樣的錯誤觀念。

    "搞硬體的比較值錢、績效比較好?"

    回覆刪除
    回覆
    1. 軟體是免費的,但人工要錢

      AI套件免費,但用AI訓練出來的資料與模型要錢

      Linux免費,但你要找一個Linux工程師幫你porting Linux driver...很貴!

      老一輩的價值觀建立在物體本身的價值上,當然會得軟體不值錢,那如果看到比特幣豈不是覺得玄上天了??

      台灣軟體工作的待遇比起當年我剛入行時好太多了,很多觀念也比當年進步,雖然說比國外還是差一截。

      軟體如果不被重視,我們公司軟體的人力就不會是硬體的三倍了,我們還是偏硬體的公司呢

      搞硬體也沒比較好過啊,也是要cost down,去凹供應商免費給sample

      終究來說,公司高層有沒有找出好的市場跟好的產品+對的人,如果公司不賺錢,軟體硬體都一樣日子難過

      過去硬體比較容易做出績效給老闆看,所以老闆覺得硬體比較重要,但現在台灣硬體上的軟體越做越大越複雜,軟體不嚴肅看待吃的虧多了,就知道要重視軟體了,現實會逼迫你進步,不然就等著被淘汰。就好比管理bug用個excel表寄來寄去,這種方式能撐多久?

      中國其實就是在走台灣的老路,為了賺錢先把智慧財產權丟一邊,老外也是故意先養肥你再來收割,中國除非回到鎖國,不然只要國際貿易還是得面對這一塊,不然中國跑去歐美註冊專利人家要不要承認呢?

      很多台商跟中國做生意還是美金交易,如果你不改,歐美聯合起來逼你改,再不改恐怕就只剩戰爭一途了。

      其實小弟春節搞這些,也只是做個消遣,順便驗證一下自己的想法,朋友給我這個機會,老闆不嫌我年紀大,就盡量做做看,已經不年輕沒辦法賭氣想換就換工作了,其實也沒有到加班的程度,以公司的營收再對比我的薪資,也看得出老闆很盡力了。

      刪除
    2. "中國其實就是在走台灣的老路..." 但不一樣的是台灣廠商少,

      老美要控訴台廠,能拿的也不多,倒不如順水推舟,讓台廠乖乖幫其代工。

      把餅做大,但中國大陸不一樣...所以同樣的作法,但結果會不同的。

      從你這段回答可以看到這個產業的縮影,

      是無奈?還是有些不是我們所看到的那個樣子?(還是有爽爽的老闆啊!)

      當然啊,最重要的還是未來吧。畢竟所有的挑戰才剛開始而已。

      刪除
    3. 大國有大國的思維,有些台灣不敢碰不願意碰的東西對岸是有意願做的,最後當然會有利益衝突,中國能搞出兩彈一星,也有能力探索月球背面,他的很多科研能力是台灣沒有的。

      不過小弟比較關心的是趨勢對我的影響是什麼?

      講真的,台灣有沒有慣老闆不重要...因為現在很多外商來台灣求才,給的待遇大都很不錯,積極一點的人就出國了,小弟這種老屁股沒辦法就只好在原地繼續打混啦

      刪除
  2. 你是用standard library才會人工設定中斷及DMA。
    HAL函式庫在使用時才決定是用中斷反應完成或是用DMA。
    不過要套用原始程式,也只能用原先用的standard library。
    我就是看完後再用HAL重造。
    HAL自帶freeRTOS及LwIP。可以省去很多設定。

    回覆刪除
    回覆
    1. 感謝您的回應與資訊~

      小弟玩STM32是玩票性質,我的工作還是傳統ARM7/9

      所以說是玩STM32,其實是想了解 lwIP,所以太自動對我沒好處,必須靠自己的力量走一遍才能抓到手感,透過動點小手術加深印象與了解

      跟這本書的想法有些類似
      微精通:從小東西學起,快快學,開啟人生樂趣的祕密通道
      https://www.books.com.tw/products/0010776629

      刪除