公告版位
從小害怕寫作文, 文筆不佳到現在, 還請各位讀者大大:
1. 發現有錯誤, 請留言告知. (或者你 '覺得' 不對也行)
2. 用字措辭不當, 請留言告知.
3. 有看沒有懂? 幫到忙也好, 幫倒忙也罷, 總之留個言吧.

通常, 嵌入式系統 (Embedded System) 都是為了執行特定工作/功能而設計的. 為了達成這些特定的任務, 系統會有一些必要的 IO 晶片/模組: (使用外加的 IO 晶片或者是 MCU 內建的 IO 模組)

  • 數位輸出/輸入類.
  • 類比輸出/輸入類.
  • 整合式的輸出/輸入模組.
    有些模組可能市場需求量很大, 或者會需要比較複雜的計算/控制, 或者和某些專利有關的, 它們有時會內含一顆 MCU 負責基本的訊號接收及計算再以特定的介面輸出(註一). 例如: GPS 模組, 或者內含三軸加速計, 三軸陀螺儀, 三軸磁力計 (電子羅盤) 的九軸 MEMS (Microelectromechanical Systems) 感測器模組, 有些模組還多了第十軸: 氣壓計. 這些模組經常是利用 MCU 的擴充匯流排介接, 例如: SPI 介面, I2C 介面. 也有少部份會以 UART 介面介接.
  • 傳輸擴充模組.
    傳輸協定簡單的傳輸模組通常內建在 MCU 晶片上 (例如: UART). 而傳輸協定複雜的傳輸模組則通常會內含一顆 MCU 來負責傳輸協定的部份. 傳輸模組種類非常多, 以實體連接線來分有:無線和有線. 有線的常用的有: UART, ethernet. 無線的從距離來分有: BLE, bluetooth, ZigBee, wifi, 2G/3G/4G. 或者單純只有實體層的無線傳輸模組 (440MHz, 2.4GHz 等 ISM Band 都有). 下列是市面上常見的傳輸模組 (Arduino 常用的傳輸模組): 440MHz 無線傳輸模組 (APC220/APC230), ethernet 傳輸模組 (ENC82J60), wifi 模組 (ESP8266), Bluetooth 模組 (HC-05, HC-06, HC-09), BLE 模組 (HM-10, HM-11), ZigBee 模組 (XBee, CC2530)...

註一: 其實這些內含 MCU 的 IO 模組本身也是一個嵌入式系統.

不過, 除了特定的系統任務, 嵌入式系統同時也多半會附有一個簡單 (或者複雜) 的人機介面, 主要的作用是:

  • 提供系統運作所必需的設定
  • 指示系統運作現況
  • 指示系統告警/錯誤 (以及告警/錯誤的確認或處理)

雖然人機介面並不是系統的主要功能, 但是人機介面的型式、種類及數量也是會影響到嵌入式系統本身的軟體架構.

一般人機介面常用的輸入元件有:

  • 開關類: jumper, DIP Switch (雙刀式), 或者是旋紐式的編碼 switch (用於不會經常變動的設定部份).
  • 按鍵類: 簡單的幾個 key, 或是組成一個 keypad. 還有最近比較夯的觸控式按鍵.

這二類是最基本的輸入元件, 大部份這二類的輸入動作都是利用嵌入式系統本身的 MCU/CPU 來完成.

另外, 還有一些功能比較複雜 (或者比較先進/內含專利) 的輸入晶片/模組也會內附一顆專用的 MCU 來處理輸入訊號, 之後再利用特定的傳輸介面傳給外部的系統(註二), 例如:

  • 大型 keyboard (PC 用的那一種)
  • 滑鼠, 或類似滑鼠的觸控板, 或是互動式觸控螢幕 (touch screen).
  • 可以辨識手勢控制的觸控板, 或是即時影像的手勢控制...
  • ...

註二: 由於功能上它是使用 '特定傳輸介面' 和另一顆外部 MCU 介接, 所以省去 '人機介面' 不用, 而改以對應式的 '暫存器' 來儲存設定, 輸入, 告警...等不同資訊. 而它和外部 MCU 常用的特定介接介接有: SPI, I2C, UART, USB...

人機介面的輸出部份一般簡單常用的類型有:

  • LED: 單顆, 單顆多色, 七段式, 米字型, 點陣組.
  • LCM: LCM 是液晶顯示器 (LCD, Liquid Crystal Display) 的控制模組 (LCD Control Module).
    簡單的有內含 5x7 字元字型的 LCD Module: 2/4 排字, 每排有 16/20/40 字. 有些也內含正體中文及簡體中文字型
    或者小型的 (1.5" ~ 3.5") 單色/彩色點陣式面板, 解析度則各式各樣都有 (主要是因為以前的 2G 手機大量使用): 84x48, 128x64, 128x128, 320x240...
  • ...

有時會因為系統的特性改用 OSD (On Screen Display), 比較高階複雜的系統可能會有繪圖晶片 (Graphic IC or GPU).

如何以正確的架構驅動這許多種不同的 IO 晶片/模組 (包括為了達成系統任務本身而必需使用的 IO, 以及人機介面的 IO), 往往是嵌入式系統工程師最頭痛的問題. 接下來, 我們要由最簡單的結構開始, 因應各種不同類型的 IO 晶片/模組, 逐步的調整, 發展出我們的 (軟體) 系統架構來.

基於在嵌入式系統上 (Embedded System) 常用的基本系統架構不外乎二種:

  • 不使用作業系統.
  • 使用即時作業系統 (RTOS).

我們先從比較單純的 '不使用作業系統' 的情況談起, 再擴展到 '使用即時作業系統' 的情況.

直觀的結構: 無窮迴圈


由於嵌入式系統的特性是持續的執行特定的工作/功能, 所以不論有沒有使用 RTOS, 軟體架構上我們都一定可以看到所謂的無窮迴圈 (infinite loop).

如果用 C 語言來實作這個無窮迴圈看起來會是像下面這樣(註三註四):

    init_this();
    init_that();
    ...
    while(1) {
        do_this();
        do_that();
        ...
    }

註三: 在沒有使用 RTOS 的狀況下, 這一段 code 會塞在 main() 裡面. 如果使用了 RTOS 則這一段 code 會存在主行程 (main process/task) 之中.

註四: while(1) 也可以換成 for(;;).

如果以時間軸來檢視各個 function 的執行, 可能會是下面這樣:

infinite_loop_basic

時間軸: 無窮迴圈的執行

For Arduino

如果你只用過 arduino, 我們可以把它轉成 Arduino Sketch:

void setup() {
    init_this();
    init_that();
    ...
}

void loop() {
    do_this();
    do_that();
    ...
}

稍微比對一下大家應該很快的就看出來它們之間的關係: (其實 Arduino 的主結構就是一個無窮迴圈啊!)

  • Sketch file 裡的 setup() 要做的事就是進入 while 迴圈之前要做的 (初始化的) 工作.
  • Sketch file 裡的 loop() 要做的就是 while 迴圈裡的工作.

或者也可以反過來, 用一般 C 語言的觀點來看 Arduino Sketch:

void main(void)
{
    setup();        // function setup() in arduino sketch
    while(1) {
        loop();     // function loop() in arduino sketch
    }
}

或許你剛剛接觸 embedded system, 對上面說的概念還有些模糊. 我們再把電腦 "輸入-處理-輸出 (I-P-O)" 的概念套進來, 應該會比較清楚一點, 變成這個樣子:

    init_input();
    init_processes();
    init_output();

    while(1) {
        do_input();
        do_processes();
        do_output();
    }

或者舉一個更接近實際應用的例子: 系統輸入部份有一個 keypad (系統及人機), 輸出部份有一些 LED 和 Relay, 還有一個 2x16 的 LCD Module (人機).

    // Init 的順序不一定要和迴圈中的執行順序一樣.
    init_outPort();  // Relay & LEDs
    init_2X16LCM();
    init_keypad();
    init_processes();

    while(1) {
        // 執行順序通常是:輸入->處理->輸出
        // 輸入 input
        read_keys();

        // 處理 process
        do_processes();

        // 輸出 output
        out_Relay();
        out_LED();
        out_2X16LCM();
    }

這裡要注意的是我們的 while(1) 裡只會有一組 I-P-O (各個 IO 晶片/模組的輸入/輸出都只會呼叫一次). 而不是直觀式的, 非常細節的, 一連串輸入-處理-輸出的組合.

    // NG Example
    while(1) {
        read_keysA();           // 輸入
        if (KeyAPressed()) {
            do_process_keyA();  // 處理
            out_Relay1();       // 輸出
            out_LED1();         // 輸出
        }
        read_keysB();
        if (KeyBPressed()) {
            do_process_keyB();
            out_Relay2();
            updateB_2X16LCM();
        }
        ...
    }

或許有人會想: 邏輯上按鍵的處理方式應該是一個鍵接著一個鍵處理, 也就是 '檢查 X 鍵有沒有被按下? 如果有, 就執行 X 鍵對應的處理邏輯, 相關的輸出需求也是在處理邏輯時就直接輸出', 然後再接著處理下一個按鍵不是嗎? 那到底程式要怎麼寫才能變成只有一組 I-P-O 的樣子啊?

是的, '一個鍵接著一個鍵處理' 的邏輯思維是正確的. 不過, 如果我們在 do_processes()do_input() 之間加入一組相互對應的 Input_Buffer 作為雙方溝通的介面: do_input() 把讀取到的輸入值存到 Input_Buffer; do_processes() 也把處理的對象改成對應的 Input_Buffer, (output 部份也是類似), 就可很輕易的作出上面的架構. 所以我們的while(1)裡面要填入的永遠是:

  • do_input(): 把所有的輸入讀進來放在 Input_Buffer.
  • do_processes(): 針對 Input_Buffer 檢查每一個對應的輸入, 然後執行對應的動作. 需要改變輸出也是放在 Output_Buffer.
  • do_output(): 把 Output_Buffer 的內容寫到輸出接腳或者是輸出晶片/模組上.

對照上面的例子: 我們把其中的 do_input() 換成 read_keys(); 把其中的 do_output() 換成 out_Relay(), out_LED(), out_2X16LCM() 三個函數的呼叫.

do_processes() 則可以改寫成像下面這樣:

void do_processes(void)
{
    if (isKeyPressed(key_A))
        do_processA();
    if (isKeyPressed(key_B))
        do_processB();
    ...
}

這樣子的寫法有許多好處:

  • 輸入同步化可以比較容易就達成: 可以經由接腳的安排 (使用同一個 GPIO port 的不同 bit 接腳) 一次就把需要同步化的輸入訊號一起讀進來.
  • 輸出同步化亦同: 同樣把需要同步化的輸出訊號安排在同一個 GPIO port 上, 這樣一個輸出指令即可以把輸出值同時寫到輸出埠上.
  • 直接利用緩衝區 (Buffer) 除去輸入-處理-輸出它們三者之間的相依性, 讓軟體和硬體可以更加獨立, 而不會被特定的 IC 或者 IO 模組綁死, 方便更換 IO 模組, 或者移值到其他硬體. 例如:
    • Keypad 原本直接使用 MCU 的 GPIO 接腳來實作, 現在需求所有更動, 需要擴充 keypad, 但 MCU 的 GPIO 接腳卻不夠了, 必需改為 IO 擴充 IC. 像這樣的情況, 我們只要修改 read_keys() 即可因應, 而不必整個專案的程式全部都翻修一遍.
    • 又或者 LED output 模組因為共同零件的關係, 修改電路並更換了 LED 零件規格, 同時驅動的方式由本來的輸出 high 點亮 LED 改為輸出 low 來點亮 LED. 我們的軟體只要修改真正的輸出程式, 在輸出之前把整組 output_buffer 執行一次 ~ (bitwise NOT) 運算就可以了.

當然, 對於一般的按鍵和 LEDs 使用位元對應的 IO buffers 當然沒有大問題, 但是這樣的寫法卻無法一體適用於所有類型的 IO 晶片/模組. 例如:

  • 非同步傳輸介面 UART, 或是使用 I2C 介面的晶片. 這類應用的流程上必需加入延遲的介面.
  • 又或者像 LCM 模組這種有特定驅動步驟的...(要先定位 cursor, 再輸出顯示資料)
  • 還有 MCU 的處理能力比資料寬度還小也會很容易就造成問題 (像是: 資料讀取到一半時被更新資料的訊號叉斷了). 例如: MCU 是 8 位元的, 但資料寬度卻是 32 位元, 或是 64 位元, 或者是使用 struct 定義的資料結構.(還沒有開始使用中斷, 這個問題在這樣的架構上不會出現.)

不過, 後面會再介紹如何使用其他技巧來解決這些問題, 使我們可以延續這個 I-P-O 架構.

下一篇我們將對這個無窮迴圈進行第一次的進化: 定期執行 IO

連結


下一篇: 嵌入式系統之軟體架構-2 定期執行 IO

文章標籤
創作者介紹
創作者 MagicJackTing 的頭像
MagicJackTing

傑克! 真是太神奇了!

MagicJackTing 發表在 痞客邦 留言(1) 人氣()


留言列表 (1)

發表留言
  • Roy
  • 受益良多,請問後續還會繼續嗎?
    謝謝!!


  • 第二篇很早就寫差不多好了,還差一些圖片及順稿校稿
    因為暫時沒空,所以一直沒放上線

    MagicJackTing 於 2018/03/15 15:47 回覆

您尚未登入,將以訪客身份留言。亦可以上方服務帳號登入留言

請輸入暱稱 ( 最多顯示 6 個中文字元 )

請輸入標題 ( 最多顯示 9 個中文字元 )

請輸入內容 ( 最多 140 個中文字元 )

請輸入左方認證碼:

看不懂,換張圖

請輸入驗證碼