LED 點陣 NTP 時鐘 (二)
上回提到,我打算用 ESP8266 來控制 DM163 和 shift register來驅動點陣 LED,但卻可能面臨 I/O 接腳不夠用的問題。為此,我幫 ESP8266 洗了一張小板子,要來搞清楚 ESP8266 在各種模式下 I/O 接腳的行為和限制。
上面那張照片就是這個小板子,其實上面沒什麼東西,除了 3.3V 的 regulator 外,就只有 reset 和控制 download mode 的按鈕,以及一些把接腳拉出來的連接器。
ESP8266 本身實在太完整了,只要加上 crystal、crystal 的負載電容、以及 SPI flash memory,就可以開機了。我覺得它最猛的是,晶片本身的 RF port 輸出阻抗居然是 50ohm 標準阻抗,如果 PCB 的阻抗控制做得好的話,甚至完全不用 matching network 就可以直接接到 IPEX connector 或是陶瓷天線。
ESP-12F 模組裡面包了 crystal、SPI flash、和很典型的 2.4GHz meander line 印刷天線,因此原則上只要供電它就會動了。我做的小板子只是把燒錄需要的訊號拉出來,以及加上一個 3.3V regulator 以便我可以用 micro USB 接頭供電。
這種簡單的板子畫起來超快,我花在建 ESP-12F footprint 的時間可能比實際走線的時間還久。但板子送洗之後我才發現我犯了一個錯:ESP-12F 印刷天線附近的鋪銅應該要挖空但我忘了挖掉,這應該會嚴重影響天線的表現。幸好 RF 的 performance 不是這次測試的重點,這張板子主要還是用來搞清楚 I/O port 的行為。
初次燒錄
我拿了一條 USB 轉 UART 的 cable,接上 ESP8266 的 UART 介面。其實這是我第一次燒錄自己設計的 ESP8266 板子,因為之前都很偷懶,直接在帶有 USB-t0-UART 介面的 NodeMCU 或 WEMOS D1 上開發。這些板子除了有 USB 轉 UART 的介面外,還會順便用 RTS 和 DTR 去控制 ESP8266 的 nRST 和 GPIO0,讓它進入燒錄模式,因此在 Arduino IDE 中只要按下燒錄鍵就好了,剩下的事情都會自動完成。
但我想觀察 ESP8266 在 boot 時的行為,所以沒有把自動燒錄的電路做上去,反而把 nRST 和 GPIO0 拉出來到按鈕上,這樣我就可以手動控制它的開機模式。
這是 ESP8266 原廠文件中說明的開機模式:
GPIO0 | GPIO2 | GPIO15 | Mode |
H | H | L | Boot from flash |
L | H | L | UART download |
x | x | H | SD boot mode |
GPIO15 為 low 時,GPIO0 可以用來選擇要從 flash 開機,還是進入 UART download 模式。UART download 模式就是我們用來燒錄 firmware 到 ESP8266 的 SPI flash 用的模式,因此我把一顆按鈕接在 GPIO0,只要在 reset 或開機時按著這顆按鈕,就會讓 ESP8266 boot 到 download 模式。
不管哪個模式,GPIO2 都必須為 high,Espressif 原廠的文件中並沒提到 GPIO2 為 low 時有什麼功用,只說那是 invalid mode,因此細節不得而知。
當 GPIO15 為 high 時,可以選擇 “SD boot mode”,這個模式因為它的名字相當誘人,在 ESP8266 的社群中有廣泛的討論,但原廠完全沒有提供關於這個模式的任何資訊。大家本來以為在這個模式下,ESP8266 可以用 SPI 從標準的 SD 卡中載入 firmware 來開機,取代原來的 SPI flash,也有人真的把模組上的 SPI flash 拔了並接上 SD slot 來做實驗,但在原廠文件缺乏的狀況下都沒什麼進展。
直到有人發現 ESP8266 跟 Espressif 的另一顆 Wi-Fi SoC ESP8089 可能是共用 die,而 ESP8089 是一個 SDIO 的 Wi-Fi SoC,大家才恍然大悟,原來所謂 “SD boot mode” 其實是 SDIO boot mode,也就是 ESP8266 會變成一個 SDIO 的 Wi-Fi 裝置。至於這個模式下的 firmware,則需要在 boot 過程中從 host 由 SDIO 介面載入到 ESP8266 的 RAM 裡面來執行。
在 HACKADAY.IO 上有篇文章,它的作者成功地用 SDIO 介面把 ESP8266 接上 Raspberry Pi 當作 Wi-Fi adapter 來使用,並且用了 ESP8089 的 Linux driver 來驅動它。
初次閃耀
我習慣用 Arduino 內建 example 中的 “Blinky” 來測試板子或是電路會不會動,於是我在 GPIO15 上焊了一顆 LED,寫了個簡單的程式測試 GPIO 跟 UART:
int IO=15; // GPIO pin to be tested void setup() { pinMode(IO, OUTPUT); Serial.begin(115200); } void loop() { digitalWrite(IO, HIGH); // LED on Serial.println("High"); delay(1000); // wait for a second digitalWrite(IO, LOW); // LED off Serial.println("Low"); delay(1000); }
沒有意外,燈會閃,UART 也有訊息吐出來。
I/O 速度
接下來我想要了解一個問題:ESP8266 Arduino core 的 I/O 可以跑多快 ?
I/O 的速度牽涉到我能不能用類似 shift register 的方式做 I/O extension,因此我想知道 ESP8266 在 Arduino core 的環境下,到底跑起來有多快。
ESP8266 裡面的 CPU 是 Tensilica 的 L106。我第一次看到這規格時,覺得 Espressif 真是給自己找麻煩,幹嘛不用業界最常用的 ARM 架構處理器呢 ? 但後來發現 Espressif 的 CEO Swee Ann Teo (張瑞安,他是現居上海的新加坡人) 在創立 Espressif 之前,曾在 Tensilica 工作,就不覺得意外了。
ESP8266 裡面的 CPU clock 標準是 80MHz,可以超頻到 160MHz。Tensilica 的 Xtensa CPU 架構是一種 “configurable” 的處理器核心,設計者可以根據需求配置不同的資源形成大小能力不同的處理器,看起來確實比 ARM 的核心要有彈性一些。雖然沒有辦法直接比較,但一般認為 ESP8266 裡的處理器能力大概跟同時脈的 ARM7TDMI-S 或 Cortex-M3 差不多。
不過處理器的速度不是我關心的重點,我想知道的是 I/O 究竟可以跑多快。
於是我用了最簡單的方法測試 I/O 速度的極限:
int IO=14; // GPIO pin to be tested void setup() { pinMode(IO, OUTPUT); } void loop() { while(1) { digitalWrite(IO, HIGH); // LED on digitalWrite(IO, LOW); // LED off } }
不加 delay、不做任何事情,就用最快的速度去切換單一 I/O 接腳,看看它跑起來多快。
在 CPU 速度設定 80MHz 的狀況下,GPIO 腳上的方波頻率大概是 1MHz,也就是 CPU clock 的 1/80。
以這個處理器的速度來說,這樣的 I/O 速度不能算快。我以前用 NXP 的 Cortex-M0 MCU LPC1343 在開發 USB 裝置時,曾經在 72MHz 的 CPU clock 下做到 4MHz 的 GPIO 切換速度。不過 LPC1343 的 GPIO 有特別設計過,它們是直接接在 AHB-lite bus 上,不像其他的 peripherals 是接在速度比較慢的 APB 上,因此 GPIO 的速度不會受到 APB 頻寬的限制。
不過我對 ESP8266 Arduino core 的運作方式也還不是那麼清楚,只能猜想 Arduino 程式應該不太可能 compile 成 ESP8266 的 native code 來跑,中間應該還有一層類似 virtual machine 或是 interpreter 之類的 middle ware。不管再小的 Arduino 程式,在 ArduinoIDE 下 compile 出來都是 200KB 起跳,這可能就是 middle ware 的足跡。因此就算 GPIO 沒有受到硬體架構頻寬的限制,軟體執行上大概也沒辦法像 native code 這麼快。
(2018.04.12 更新:下面有讀者留言指正,ESP8266 Arduino core 編譯出來的 binary 確實是 ESP8266 的 native binary,那 200KB 是 SDK 的 footprint。至於 I/O 很慢的問題,則是 Arudino core 本身的 overhead,對 ESP8266 或 AVR 都一樣。非常感謝這位讀者的指正,沒想到我的部落格真的有人在認真看耶 T_T)
Espressif 有開放 ESP8266 的 native 開發環境,但我還沒有時間把它的 tool chain 建起來,因此沒辦法試試在 native 環境下的 I/O 速度。
但我想到我可以用同樣的方法來試一下 ATmega328 的 I/O 速度。ATMega168/328 是Arduino 最早開發出來的平台,Arduino Uno 跟 Mini/Nano 都是用這顆 MCU。於是我找了一片 Arduino Nano,把同樣的程式碼 compile 給 ATmega328 來跑。
在 ATmega328 跑 16MHz 的狀況下,I/O 切換的速度是 111KHz 左右,差不多是 CPU clock 的 1/140。
就比例上來說,這個速度比 ESP8266 Arduino core 的表現還差,顯然 ATmega 在 Arduino 上的效能距離 native code 更遠了。
最後我還想搞懂一件事:跟 flash 的 SPI 介面共用的 GPIO9/GPIO10 到底能不能拿來用?
兩條線或四條線?
ESP8266 的 flash 有兩種工作模式: 使用四隻資料接腳的 QIO 模式和只使用兩隻資料接腳的 DIO 模式。GPIO9 和 GPIO10 剛好對應到 QIO 模式下的 D2 和 D3 兩條資料線,而這兩條線在 DIO 模式下是沒有用到的。根據許多討論區內的說法,只要在 Arduino compile 時選擇 DIO 模式,建出 DIO 模式下的 binary 燒進去,GPIO9 和 GPIO10 就可以拿來用。
做實驗總是要有對照組,於是我先 compile 了一個 QIO 模式下的 binary,想看看執行的時候 GPIO9 或 GPIO10 上面會不會有 SPI flash 的訊號。用示波器去觀察 QIO 模式下的 GPIO9,果然可以看到像 SPI data 的訊號。
我把同樣的程式以 DIO 模式 compile,再燒進去跑,不意外地,GPIO9 就變得靜悄悄了。
有趣的是,SPI 上的訊號只出現在剛開機的一小段時間內,接下來不管程式怎麼跑,SPI 上都靜悄悄的沒有訊號。
由於原廠文件的缺乏,關於 ESP8266如何執行存在 SPI flash 中的程式碼,其實還有很多待解的謎團。社群中有很多高手深入地去挖掘 native SDK,並交叉比對處理器的行為,稍微拼湊出一點輪廓。
ESP8266 有一塊 on-chip 的記憶體叫做 IRAM (對,跟 ARM 的那個 IRAM 一樣意思,就是 instruction RAM) 可以用來跑程式,不過這一塊 IRAM 的大小只有 32KB,至於哪些程式碼要放到這裡面來執行,則是靜態地在編譯時就由 linker 根據連結的 segment 決定。使用者可以在宣告 function 時加上一些修飾的保留字,讓那個 function 會被載入到 IRAM 執行。
但根據 ESP8266 社群中很多高手的研究,SDK library 中大部分的 function 都宣告成在 IRAM 中執行,如果你用了很多 SDK library 中的 function,特別是跟 Wi-Fi 有關的,那 IRAM 可能就被這些 SDK library 的 function 給佔滿了,當使用者宣告 function 要載入 IRAM 執行但已經沒有足夠的 IRAM 可以用的時候,linker 就會給出記憶體不足的錯誤。
除了 IRAM 之外,ESP8266 還有一塊 32KB 的 RAM,用來當作 SPI flash 的 cache,它就像一般的 instruction 一樣運作: 常用的部分會被留在 cache 裡面,不常用到的就會被踢出去。如果 SPI flash 裡的程式碼被載入到 cache 中執行,除了載入時所造成的 delay 之外,它的執行速度是跟在 IRAM 中跑一樣快。
這塊 cache 也可以關掉不用,ESP8266 仍然有能力直接從 SPI flash 上執行程式,這種做法就是傳說中的 XIP: execution in place。但不管 SPI 再快,還是比位於 internal bus 上的 IRAM 慢上一大截。根據許多高手的實驗,直接從 SPI 上跑程式大概比從 IRAM 上面跑慢 12 到 13 倍。
我在 SPI介面上所看到的行為應該就是開機時 bootloader 先從 SPI flash 中把要在 IRAM 裡面執行的 code 載入 IRAM,以及第一次的 cache miss 所造成的 cache reload。接下來因為那個迴圈的程式碼很小,它應該整段都已經被載入 cache 中,而且怎麼跑都沒有 cache miss 發生,所以 SPI 介面上就不再有訊號了。
確定 GPIO9/GPIO10 在 DIO 模式下應該是安全的之後,我就可以來試試使用它們了。
我把前面閃燈的程式改到 GPIO9,用 DIO 模式編譯,執行。
結果當掉。
ESP8266 發出 watchdog timer reset 的訊息,然後自動重開。
怪了,我剛剛明明已經確認過在 DIO 模式下 GPIO9 上面沒有訊號,應該是安全的啊?
試試 GPIO10 好了。
我再把閃燈程式改到 GPIO10,用 DIO 模式編譯,執行。
居然可以跑!
更怪了,為什麼 GPIO10 可以用,但 GPIO9 不能用呢?
這時我腦中 “登” 的一下亮起了一顆燈泡: 我應該要查一下 GPIO9 跟 GPIO10 接到 SPI flash 的哪兩隻腳上。
原來 GPIO9 接到 SPI flash 的 HOLD 接腳,而 GPIO10 接到 WP 接腳。雖然在 QIO 模式下這兩隻腳已經沒有 SPI 的訊號,但它們還是跟 SPI flash 連接在一起。我在 GPIO 上做的任何動作都會直接反應在 SPI flash 的接腳上。WP 是控制 SPI flash 寫入保護的接腳,對讀取來說不痛不癢,因此就我去 toggle GPIO10,讓 WP 接腳跟著一起變化,也不會影響 SPI flash 的讀取。
但接在 HOLD 腳上的 GPIO9 就沒這麼簡單了。HOLD 腳是在 multi-slave 的 SPI bus 中用來暫停特定 slave 的訊號,當 HOLD 被拉低,即使 SPI clock 還在跑,SPI flash 也會停止動作直到 HOLD 訊號恢復為止,因此 GPIO 上的狀態會透過 HOLD 接腳影響 SPI flash 的運作,難怪會讓 ESP8266 當掉。
為了要驗證這個想法,我想到我可以把 GPIO9 跟 SPI flash 之間的線路切斷,再試試看去動 GPIO9 會不會導致當機。
ESP-12F 模組有 shielding can,要碰得到 SPI flash 之前得先把 shielding can 拆掉,但我現在沒有熱風槍可以用,只用烙鐵要拆這蓋子幾乎是不可能的事。我找了一下手上的其他模組,有個 ESP-01 是沒有 shielding can 的,於是我很快用洞洞板幫 ESP-01 裝了一個燒錄電路,打算用它來做實驗。
把該接的線都接好,試過可以燒錄之後,我就用烙鐵小心地把 SPI flash 的第七隻腳 HOLD 挑起來讓它離開電路板,這隻腳就是連接到 GPIO9 的哪隻,理論上我把它跟 GPIO9 斷開後再去動 GPIO9,就不會影響 SPI flash 的運作,在 DIO mode 下也不會讓 ESP8266 當機。
我如法炮製,執行前面測試 GPIO9 的程式,並監看 UART 上的輸出。因為之前動用 GPIO9 造成的當機會觸發 watchdog timer reset,然後系統會自動重開,如果不看 UART 上的錯誤訊息,會無法判斷到底有沒有觸發 WDT reset。
果然,把 GPIO9 跟 SPI flash 斷開後,GPIO9 就可以運用自如了。
這篇先寫到這裡,AppWorks Demo Day #14 的 dress rehearsal 即將開始,我要去準備直播的設備了。
您好,
我想指正一下文中提到” Arduino 程式應該不太可能 compile 成 ESP8266 的 native code 來跑,中間應該還有一層類似 virtual machine 或是 interpreter 之類的 middle ware。”這部份猜測是錯誤.
實際上編譯出來的確是native code(不管是AVR還是ESP8266)
至於ESP compile出來200KB起跳的原因是SDK library本身的code就那麼肥…
如果想減少code size可以參考一下cnlohr的nosdk8266 (https://github.com/cnlohr/nosdk8266)
另外IO切換速度很慢的原因是Arduino core本身的overhead,
直接對GPIO的暫存器操作會快上不少(關鍵字: arduino io speed)
GPIO的切換速度對於MCU來說是很重要的一項指標
很少有切換IO需要32 cycle以上的架構(clock沒破100MHz情況下)
實際上並沒有您說的那麼不堪@@
多謝指正,已於文中加註