做爲前端工程師,咱們編寫的代碼只能活在瀏覽器、小程序或者 Node 進程裏,這彷佛已經成爲了一種常識。但這就是咱們的能力邊界了嗎?本文將帶你爲一臺內存僅 32M,分辨率僅 320x240 的掌上游戲機適配前端工具鏈,見證 Web 技術棧的全新可能性。前端
本次咱們的目標,是隻配備了 400Mhz 單核 CPU 和 32M 內存的國產懷舊掌機 Miyoo。它當然徹底沒法與如今的 iOS 和安卓手機相提並論,但卻能很好地在小巧精緻的體積下,知足玩小霸王、GBA、街機等經典遊戲平臺模擬器的需求,價格也極爲低廉。這是它和 iPad mini 的對比圖:linux
那麼,怎樣纔算是爲它移植了一套前端技術棧呢?我我的的理解裏,這至少包括這麼幾部分:git
下面將逐一介紹爲完成這三大部分的移植,我所作的一些技術探索。這主要包括:github
Let’s rock!web
入門嵌入式開發時咱們首先應該作到的,就是將源碼編譯爲嵌入式操做系統上的應用。那麼 Miyoo 掌機的操做系統是什麼呢?這裏首先有一段故事。docker
Miyoo 是個國內小公司基於全志 F1C500S 芯片方案定製的掌機,其默認的操做系統是閉源的 Melis OS,在國外以 Bittboy 和 Pocket Go 的名義銷售,小有名氣。閉源系統天然不能知足愛好者的需求,所以社區對其進行了逆向工程。來自臺灣的前輩司徒 (Steward Fu) 成功將 Linux 移植到了這臺掌機上,但惋惜他已因我的緣由退出了開發。如今這臺遊戲機的開源系統 MiyooCFW 基於司徒最先移植的 Linux 4.14 內核,由社區維護。編程
所以,咱們的目標系統既不是 iOS 也不是安卓,而是原汁原味的 Linux!如何爲嵌入式 Linux 編譯應用呢?咱們須要一套由編譯器、彙編器、連接器等基礎工具組成的工具鏈,以構建出可用的 ARM 二進制程序。小程序
在各個操做系統上搭建開發環境,每每至關繁瑣。如今開源掌機社區中流行的方式是使用 VirtualBox 等 Linux 虛擬機。這基本解決了工具鏈的跨平臺問題,但尚未達到現代前端工程的開發便利度。所以我選擇首先引入 Docker,來實現跨平臺開箱即用的開發環境。瀏覽器
咱們知道,Docker 容器能夠理解爲更輕量的虛擬機。咱們只要一句 docker run
命令就能運行容器,併爲其掛載文件、網絡等外部資源。顯然,如今咱們須要的是一個【能編譯出嵌入式 Linux 應用】的 Docker 容器,這能夠經過製做出一個用於啓動容器的基準 Docker 鏡像來實現。Docker 鏡像很容易跨平臺分發,所以只要製做並上傳鏡像,基礎的開發環境就作好了。bash
那麼,這個 Docker 鏡像中應該包含什麼內容呢?顯然就是編譯嵌入式應用的工具鏈了。司徒已爲社區提供了一套在 Debian 9 上預編譯好的工具鏈包,只須要將其解壓到 /opt/miyoo
目錄下,再安裝一些常見依賴,就能夠完成鏡像的製做了。這一過程能夠經過 Dockerfile 文件來自動化,其內容以下所示:
FROM debian:9
ADD toolchain.tar.gz /opt ENV PATH="${PATH}:/opt/miyoo/bin"
ENV ARCH="arm"
ENV CROSS_COMPILE="arm-miyoo-linux-uclibcgnueabi-"
RUN apt-get update && apt-get install -y \ build-essential \ bc \ libncurses5-dev \ libncursesw5-dev \ libssl-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /root 複製代碼
這樣只要用 docker build
命令,咱們就能用純淨的 Debian 鏡像製做出純淨的嵌入式開發鏡像了。那麼接下來又該如何用鏡像編譯文件呢?假設咱們作好了 miyoo_sdk
鏡像,那麼只要將本地的文件系統目錄,掛載到基於鏡像所啓動的容器上便可。像這樣:
docker run -it --rm -v `pwd`:/root miyoo_sdk
複製代碼
簡單說來,這條命令的意義是這樣的:
docker run
基於 miyoo_sdk
鏡像啓動一個臨時容器-v
將當前目錄掛載到容器的 /root
下-it
讓咱們用當前終端來登陸操做容器的 Shell--rm
使容器用完即棄,除更改當前目錄外,不留任何痕跡所以,咱們實際上基於 Docker,直接在容器裏編譯了 Mac 文件系統上的源碼。這既沒有反作用,也不須要其餘數據傳遞操做。對於日益複雜的前端工具鏈依賴問題,我相信這也是一種解決方案,有機會能夠單獨撰文詳述。
Docker 鏡像製做好以後,咱們就能用上容器裏 arm-linux-gcc
這樣的編譯器了。那麼該怎麼編譯出一個 Hello World 呢?如今還沒到引入 JS 引擎的時候,先用 C 語言寫出個簡單的例子,驗證一切都能正常工做吧。
嵌入式 Linux 設備經常使用 SDL 庫來渲染基礎的 GUI,其最簡單的示例以下所示,是否是和前端同窗們熟悉的 Canvas 有些神似呢:
#include <stdio.h>
#include <SDL.h>
int main(int argc, char* args[]) {
printf("Init!\n");
SDL_Surface* screen;
screen = SDL_SetVideoMode(320, 240, 16, SDL_HWSURFACE | SDL_DOUBLEBUF);
SDL_ShowCursor(0);
// 填充紅色
SDL_FillRect(screen, &screen->clip_rect, SDL_MapRGB(screen->format, 0xff, 0x00, 0x00));
// 交換一次緩衝區
SDL_Flip(screen);
SDL_Delay(10000);
SDL_Quit();
return 0;
}
複製代碼
這份 C 源碼能夠經過咱們的 Docker 環境編譯出來。但顯然稍有規模的應用都不該該直接敲 gcc
那堆參數來直接構建,經過像這樣的 Makefile 來自動化比較好(注意縮進必須用 tab 哦):
all:
arm-linux-gcc main.c -o demo.out -ggdb -lSDL -I/opt/miyoo/arm-miyoo-linux-uclibcgnueabi/sysroot/usr/include/SDL
clean:
rm -rf demo.out
複製代碼
除了登錄 Docker 容器的 Shell 以外,咱們還能夠經過 -d
參數輕鬆地建立「無頭」的容器,在後臺幫你編譯。像構建這個 Makefile 所需的 make
命令,就能夠在 Mac 終端裏這樣一行搞定:
docker run -d --rm -v `pwd`:/root miyoo_sdk make
複製代碼
這樣就能生成 demo.out
二進制文件啦。將這個僅有 12KB 的文件複製到 Miyoo TF 卡里的 /apps
目錄裏後,再用 Miyoo 自帶的程序安裝器打開它,就能看到這樣的結果了:
這說明 Docker 編譯工具鏈已經正常工做了!但這還遠遠不夠,如今的關鍵問題在於,咱們的 printf
去哪了?
基礎的 Unix 知識告訴咱們,進程的輸出是默認寫到 stdout 這個標準輸出文件裏的。通常來講,這些輸出都會寫入流式的緩衝區,進而繪製到終端上。可是,嵌入式設備的終端在哪裏呢?通常來講,這些日誌寫入的是所謂的 Serial Console 串口控制檯。而這種控制檯的數據,則能夠經過很是古老的 UART 傳輸器來和 PC 交互,只須要接上三條電路的連線就行。
所以,咱們須要想辦法接通 Miyoo 的 UART 接口,從而才能在電腦上登錄它的 Shell。在這方面,司徒的 焊接 UART 接頭 這篇文章是很是好的參考資料。我對其中的一句話印象尤爲深入:
廠商真是貼心,特別把 GND、UART1 RX、UART1 TX(由上而下)拉出來,提供開發者一個友好的開發界面
拆機焊接才能用的東西,在大佬眼裏竟然算是友好的開發界面…好吧,不就是焊接嗎?現學就是了。
首先咱們把後蓋拆開,再把主板卸下來。這步只須要標準的十字螺絲刀,注意別弄丟小零件就行。完成後像這樣:
看到圖中主板右上角的三根針了嗎?這就是 UART 的三個接口了(這時我還沒焊接,只是把排針擺上去了而已)。它們自上而下分別是 GND、RX 和 TX,只要爲它們焊接好排針,將導線連到 UART 轉 USB 轉換器,就能在 Mac 上登錄它啦。鏈接順序是這樣的:
因此,咱們須要先焊上排針。焊接看起來很折騰,現學起來倒並不難,其實只要先把烙鐵頭壓在焊點上,而後把焊錫絲放上去就行。像我這樣的新手,還能夠買一些白菜價的練習板,拿幾個二極管練練手後再焊真的板子。完成後的效果以下所示,多了三根紅色排針(焊點在背面,很醜就不放圖了):
焊好之後,用萬用表便可測量焊點是否接通。還記得高中物理裏萬用表的紅黑表筆怎麼鏈接嗎…反正我早就忘光了,也是現學的。實際測得 RX 和 TX 各自到 GND 的電阻值都在 600 歐姆左右,就表明鏈接暢通了。
加上轉接頭,連好以後的效果是這樣的:
最後我爲了能把機器裝回去,又在後蓋上打了個洞,像這樣:
作完這個硬件改造以後,該如何實現軟件上的鏈接呢?這就須要可以登錄串口的軟件了。Unix 裏一切皆文件,所以咱們只要找到 /dev
目錄下的串口文件,而後用串口通訊軟件打開這個文件就行啦。screen 是 Mac 內置的命令行會話軟件,但用起來較爲麻煩,這裏推薦 Mac 用戶使用更方便的 minicom。鏈接好以後,能看到形如這樣的登錄日誌輸出:
[ 1.000000] devtmpfs: mounted
[ 1.010000] Freeing unused kernel memory: 1024K
[ 1.130000] EXT4-fs (mmcblk0p2): re-mounted. Opts: data=ordered
[ 1.230000] FAT-fs (mmcblk0p4): Volume was not properly unmounted. Some data may be corrupt. Ple.
[ 1.250000] Adding 262140k swap on /dev/mmcblk0p3. Priority:-2 extents:1 across:262140k SS
Starting logging: OK
read-only file system detected...done
Starting system message bus: dbus-daemon[72]: Failed to start message bus: Failed to open socket: Fd
done
Starting network: ip: socket: Function not implemented
ip: socket: Function not implemented
FAIL
Welcome to Miyoo
miyoo login:
複製代碼
看起來已經接近成功,能夠 login 進去看日誌了吧?結果一個 bug 攔住了我:全部按鍵按下去都沒反應,徹底登錄不了終端,怎麼辦?
我歷來沒作過這種層面的硬件改造,也沒用過 UART 串口。所以這個問題對我至關棘手——既多是硬件問題,也多是軟件問題。但總該是個能夠解決的問題吧。
/etc/inittab
的配置和它啓動的 /etc/main
腳本都是有效的,排除了設備側的軟件問題。好吧,我竟然一路 debug 碰到了個物理電路設計的硬件問題。那就接着改 Linux 內核唄。
根據司徒提供的線索,我開始嘗試將音頻驅動從 Miyoo 的 Linux 內核源碼中屏蔽掉。咱們都知道 Linux 是宏內核,大量硬件驅動的源碼全都在裏面。簡單改改驅動,其實不是件多高大上的事情。
首先,咱們至少要能把內核編譯出來。注意內核不等於嵌入式 Linux 的系統。一個完整的嵌入式 Linux 系統,應該大體包括這幾部分:
咱們只是想禁用掉音頻驅動,所以只須要從新編譯出 Kernel 就行。Kernel 會編譯成名爲 zImage 的鏡像。這個過程的用戶體驗其實和編譯普通的 C 項目沒有什麼區別,也就是先配好編譯參數和環境變量,而後 make
就好了:
make miyoo_defconfig
make zImage
複製代碼
在我 MacBook Pro 的 Docker 裏,大體須要 12 分鐘才能把內核編譯出來。這裏貼個圖,記念下職業生涯第一次編譯出的 Linux 內核:
編譯經過後,我很是開心地直接開始嘗試修改內核的驅動(注意我沒有真機測試這個第一次編譯出的內核,這是伏筆)。通過一番研究,我發現嵌入式 Linux 的硬件都是經過一種名叫設備樹的 DSL 代碼來描述的,修改這種 DSL 應該就能使 Kernel 不支持某種硬件了。因而我找到了 Miyoo 設備樹裏的音頻部分,將其註釋掉,嘗試編譯出不包括音頻的設備樹描述文件,把它裝上去。
而後機器啓動後就黑屏了。
……
看來設備樹的配置無論用,我又想到了直接修改音頻驅動的 C 源碼。它就是內核項目的 /sound/soc/suniv/miyoo.c
,裏面的 C 代碼看起來並不難,但我嘗試了不下七八種修改手法,就是編譯不出一份正常的鏡像:有時候能夠解決 UART 沒法登錄的問題,有時則不行,而且黑屏問題也始終沒有解決。爲何音頻驅動會影響視頻輸出,這讓我十分困擾,甚至一度懷疑起了個人工具鏈。
最終,我獲得了一個使人震驚的結論:
這分內核代碼哪怕徹底不改,編譯出來都是會黑屏的。
……
因而,我換了社區版本的內核代碼,屏幕順利點亮,問題解決。
可是,社區版本的內核是老外維護的,他們的用戶習慣裏,A 鍵和 B 鍵的定義是相反的(小時候玩過美版 PSP 的同窗應該知道我是什麼意思)。因而我又開始折騰,嘗試如何交換 A 和 B 的位置。
結果,我遇到了一個更加詭異的問題,那就是隻要我在鍵盤驅動裏交換 A 和 B 的值,要麼不生效,要麼就總會有其它的按鍵失靈,不能徹底交換成功。
因而,我去仔細研究了按鍵驅動所對應的 Linux 內核 GPIO 部分的文檔,檢查了 init 和 scan 階段下這一驅動的行爲,甚至懷疑按鍵的宏定義會影響位運算的結果……結果都沒什麼卵用。但我仍是找到了個能顯示按鍵信息的調試用宏,以前一直懶得浪費一次編譯時間去打開它,乾脆把它啓用後再試一下。
結果,我又獲得了一個使人震驚的結論:
這份代碼把變量的名字寫錯了。該對換的變量不是 A 和 B,是 A 和 X。
……
看來我果真沒有寫 Linux 內核的天賦,仍是老老實實回去移植 JS 引擎吧。
搞定內核層之後,咱們就能夠輕鬆登陸進 Miyoo 的控制檯了。用戶名是 root,沒有密碼。繞了這麼多彎子,第一次登錄成功的時候仍是讓人很激動的。截圖記念一下:
接下來應用層的 JS 引擎移植,對我來講就是輕車熟路了。這裏祭出咱們的老朋友 QuickJS 引擎,它做爲一個超迷你的嵌入式 JS 引擎,甚至已經兼容了很多 ES2020 裏的特性。因爲它沒有任何第三方依賴,把它遷移到 Miyoo 上,其實並無多難,給 Makefile 加上個 CROSS_PREFIX=arm-miyoo-linux-uclibcgnueabi-
的編譯配置,就能夠用交叉編譯器來編譯它了。
交叉編譯天然也很難一路順風。這裏我遇到的編譯錯誤,都來自嵌入式環境下的標準庫能力缺失。不過其實也只有這兩點:
malloc_usable_size
不支持,這會影響內存度量數據的獲取,但 JS 照樣能夠跑得很歡。順便一提從源碼來看,這個能力在 WASM 裏也不支持。因此其實已經有人把 QuickJS 編譯成 WASM,玩起 JS in JS 的套娃了。fenv.h
缺失,這應該會影響浮點數的 rounding 方式,但實測對 Math.ceil
和 Math.floor
無影響。先無論了,反正又不是不能用(喂)這點小問題,簡單 patch 一下相關代碼之後就搞定了。編譯成功後,把它複製到 rootfs 分區的 /usr/bin
目錄下,便可在在 Miyoo 的 Shell 裏用 qjs
命令運行 JS 了。這下終於爽了,看我回到主場,噼裏啪啦寫段 JS 測試一下:
import { setTimeout } from 'os'
const wait = timeout =>
new Promise(resolve => setTimeout(resolve, timeout))
let i = 0
;(async () => {
while (true) {
await wait(2000)
console.log(`Hello World ${i}!`)
i++
}
})()
複製代碼
截圖爲證,我真的是在 Miyoo 裏面跑的:
可是這個 JS 代碼的運行結果又該怎麼輸出到真機上呢?咱們知道 Linux 上有默認的 /dev/console
系統控制檯和 /dev/tty1
虛擬終端,所以只要在啓動時的 inittab
裏把 console::respawn:/etc/main
改爲 tty1::respawn:/etc/main
,就能夠輸出到圖形化的虛擬終端了。像這樣:
JS 都能跑了,日誌都能看了,還要啥自行車呢?固然是支持給它下斷點啊!我原本一直覺得斷點調試必需要用 V8 那樣的重型引擎配合 Chrome 才行,結果讓我驚喜的是,社區已經爲 QuickJS 實現了一個支持調試器的 fork,這樣只須要 VSCode 做爲調試器前端,就能調試 QuickJS 引擎運行時的代碼了。配合 VSCode 的 Remote 功能,這玩意的想象空間實在很大。
這一步的支持是全文中最省事的。由於我只在 Mac 上作了個驗證,編譯一次經過,沒什麼好說的。效果像這樣:
圖中你看到的 VSCode Debugger 背後可不是 V8,而是正經的 QuickJS 引擎噢。我也用 VSCode 調試過 Dart 和 C++ 的代碼,當時我沒有想到過這樣的一套調試器該如何由一門第三方語言接入。搜索以後我發現,微軟甚至已經爲編輯器與任意第三方語言之間設計了一個名爲 Debug Adapter Protocol 的通用調試協議,它很具有啓發性。原來我以爲十分高大上的編程語言調試系統,也是能用斷點、異常等概念來抽象化和結構化,並設計出通用協議的。微軟在工程設計和文檔上的積累真不是蓋的,贊一個。
如今,我已經將這個支持 VSCode 調試的 QuickJS 版本編譯到了 Miyoo 上,只是尚未作過實際的調試——有了定製內核驅動時不停給本身挖坑的教訓,我如今天然不敢立 Flag 說它能用了(捂臉)
到此爲止,本次實驗所關注的能力都已經獲得基本的驗證了。相應的 Docker 鏡像我也已發佈到 GitHub,參見 MiyooSDK。也歡迎你們的交流。
此次寫的又是一篇長文,這整套工做遠沒有文章寫下來那麼一鼓作氣,而是斷斷續續地逐步完成的。如今我手上的東西,還只是個初步的工程原型,有不少工做還能夠繼續深刻。好比這些地方:
不過,只要有熱情持續深刻技術,那麼收穫必定不會讓你失望。像你們眼裏神祕的 Linux 內核,其實也是個有規可循的程序。即使是我這樣本職寫 JavaScript 的玩票選手,照樣能夠拿通用的科學方法論來實驗分析它,而這個過程就像玩密室逃脫或者解謎遊戲同樣有趣——你知道問題必定能解決,只要用邏輯推理,找到房間裏隱藏的那個開關就行。
我要特別感謝司徒,他爲開源掌機的發展做出了巨大的貢獻。此次最爲疑難的硬件電路 bug,也是由他提供了關鍵信息後才最終得以解決的。不少時候咱們缺的不是繁冗瑣碎的入門指南,而是來自更高段位者,一兩句話讓你茅塞頓開的點撥。他就是這樣一位使人尊敬的技術人。
這裏跑個題,淘寶上有很多用司徒系統的名義銷售掌機的店鋪,這些商家其實已經與他本人徹底無關了。雖然我仍然很推薦你們入手這個只要一百多塊錢的 Miyoo 掌機用於娛樂或技術研究,但我仍是有些感慨。所謂遍身羅綺者,不是養蠶人,大抵如此吧。
從搭建工具鏈到焊接電路板,再到定製 Linux 內核和 JS 引擎,這些技術自己當然都有點門檻。但富有樂趣的目標,總能讓咱們更有動力去克服中途的各類困難。我相信興趣和熱情老是最能刺激求知慾的,而永不滿足的求知慾,才能驅動咱們不停越過一個個山丘。畢竟喬幫主提過的那句名言是怎麼說的來着?
Stay Hungry. Stay Foolish.
我主要是個前端開發者。若是你對 Web 編輯器、WebGL 渲染、Hybrid 架構設計,或者計算機愛好者的碎碎念感興趣,歡迎關注我噢 :)
#靈感