大多數時候我們使用 Arduino 這類開發板的時候都會使用 Arduino 提供的 IDE或者像是 ESP32/ESP8266 官方提供的開發工具,不過當有一些相依的套件需要使用的時候,就不一定是那麼的方便。而 PlatformIO 彙整了大部分開發板的工具,並且提供相依套件管理跟編譯工具的自動化等處理,讓使用上變得非常容易,也很適合我們這次的開發跟使用。
PlatformIO 有兩種版本,一種是 IDE 的版本可以跟 VSCode 整合,另外一種是 CLI(Command Line Interface)的版本,文章會以 macOS 上使用 CLI 版本當範例說明,大家可以依照自身開發環境的狀況選擇偏好的做法。
在寫鐵人賽的時候 PlatformIO 已經更新到 5.0 和舊版有一些差異,文章都會用 5.0 的設定說明。
目前我們有自己的 Makefile
來輔助編譯之外,還有 src
目錄放置原始碼,在 PlatformIO 的專案結構下我們需要將 src
放到 lib/iron
下面作為 Library 使用,然後將 src/main.c
放到 PlatformIO 的 src/main.cpp
當作是 Arduino 的主程式使用。
在原本的專案中使用 pio init -b d1
這邊的 -b
是表示使用 d1
這個開發板的意思,可以用 pio boards
尋找自己手邊的有開發板來使用,我們這次是以旗標的復古遊戲機為基礎,要達到相同的條件只需要支援 Arduino 框架以及有一塊適合的 TFT 螢幕即可。
執行完畢後專案大致上會有這樣的結構
├── Makefile
├── app.rb
├── bin
├── include
│ └── README
├── lib
│ └── README
├── platformio.ini
├── src
│ ├── app.h
│ ├── irep.c
│ ├── irep.h
│ ├── iron.c
│ ├── iron.h
│ ├── khash.h
│ ├── main.c
│ ├── opcode.h
│ ├── utils.h
│ ├── value.h
│ ├── vm.c
│ └── vm.h
└── test
└── README
我們先用 rm -rf bin Makefile
把之後會由 PlatformIO 替代的部分刪除,接著用 mkdir -p lib/iron
製作放置我們 Ruby VM 實作的目錄,然後 mv src/* lib/iron/
將檔案先移動過去,最後用 mv lib/iron/main.c src/main.cpp
把主程序移動回來。
完成後專案的結構大致上會像下面這樣
├── app.rb
├── include
│ └── README
├── lib
│ ├── README
│ └── iron
│ ├── app.h
│ ├── irep.c
│ ├── irep.h
│ ├── iron.c
│ ├── iron.h
│ ├── khash.h
│ ├── opcode.h
│ ├── utils.h
│ ├── value.h
│ ├── vm.c
│ └── vm.h
├── platformio.ini
├── src
│ └── main.cpp
└── test
└── README
接下來我們需要讓 PlatformIO 可以繼續正常在電腦上做測試,因此我們會利用 test
(測試)的功能在電腦上製作一個可以運行的測試檔案,新增檔案 test/test_native/main.cpp
來撰寫最初的測試
// test/native.cpp
#include <stdio.h>
#include <unity.h>
#include "app.h"
#include "iron.h"
#include "vm.h"
void test_mrb_run(void) {
mrb_state* mrb = mrb_open();
mrb_run(mrb, app);
mrb_close(mrb);
}
int main(int argc, char **argv) {
UNITY_BEGIN();
RUN_TEST(test_mrb_run);
UNITY_END();
return 0;
}
基本上跟我們在之前的 src/main.c
沒有太多差異,但是配合 PlatformIO 測試框架進行了簡單的改寫,要注意的是這個並不是一個標準的測試,只是方便我們驗證 Ruby VM 能夠正常運作的手段。
接下來我們需要能讓 PlatformIO 在編譯前幫我們把機器碼產生,為了方便處理我們會直接生成 app.h
放到 include/
目錄下面,因此先用 rm lib/iron/app.h
把之前用來定義 uint8_t app[]
的檔案刪除,然後加入新的 Python 腳本(pre_build_ruby.py
)用於 PlatformIO 的預處理。
try:
import configparser
except ImportError:
import ConfigParser as configparser
Import("env")
config = configparser.ConfigParser()
config.read("platformio.ini")
script = config.get(f"env:{env['PIOENV']}", "custom_ruby_script")
env.Execute(f"mrbc -B app -o include/app.h {script}")
我們簡單的利用 PlatformIO 的 API 讀取了設定中的 custom_ruby_script
當作參考,決定要以哪個檔案為基礎生成機器碼,這樣也能在正式放到開發板上的檔案做區分方便測試。
接下來要修改 platformio.ini
的設定,將我們剛剛的預處理設定加入,以及增加 native
環境讓我們可以在電腦上做測試。
; platformio.ini
[env]
platform = native
extra_scripts =
pre_build_ruby.py
[env:test]
build_type = debug
build_flags = -D DEBUG
custom_ruby_script = app.rb
[env:d1]
platform = espressif8266
board = d1
framework = arduino
custom_ruby_script = app.rb
test_ignore = test_*
我們先將預設環境設定為 native
並且指定 d1
環境跳過測試,讓我們只針對本機測試,這樣就能製作出一個類似我們原本搭設的開發環境,也方便繼續擴充功能。
不過直接運行 pio test
還是會發生問題,這是因為我們原本都是以 C 語言作為前提所撰寫的,現在改為 C++ 之後需要做一些處理,目前只需要在 lib/iron/iron.h
和 lib/iron/vm.h
兩個檔案加入針對 C++ 的設定即可。
// lib/iron/iron.h
#ifndef _IRON_H_
#define _IRON_H_
#include "khash.h"
#include "value.h"
#define IRON_API extern
#ifdef __cplusplus
extern "C" {
#endif
// ...
#ifdef __cplusplus
}
#endif
#endif
// lib/iron/vm.h
#ifndef _IRON_VM_H_
#define _IRON_VM_H_
#include <stdint.h>
#include "irep.h"
#include "iron.h"
// ...
#ifdef __cplusplus
extern "C" {
#endif
// ...
#ifdef __cplusplus
}
#endif
#endif
這邊我們針對 C++ 加入了 extern "C"
的設定讓 C++ 可以順利存取到這些在 C 定義的方法,接著運行 pio test
就可以看到我們的測試運行起來,並且將除錯訊息印出來。
Collected 1 items
Processing test_native in test environment
----------------------------------------------------------------------------------------------------------------------
Building...
Testing...
[DEBUG] locals: 1, regs: 6, ireps: 0
[DEBUG] r[1] = self 0
[DEBUG] r[2] = 10
[DEBUG] r[2] = r[2] + 5
[DEBUG] r[3] = mrb_int(2)
[DEBUG] r[4] = mrb_int(3)
[DEBUG] func puts not found
[DEBUG] return r[1]
test/test_native/main.cpp:16:test_mrb_run [PASSED]
-----------------------
1 Tests 0 Failures 0 Ignored
OK
============================================= [PASSED] Took 0.84 seconds =============================================
Test Environment Status Duration
----------- ------------- -------- ------------
test_native test PASSED 00:00:00.842
test_native d1 IGNORED
============================================ 1 succeeded in 00:00:00.842 ============================================
現階段運行 pio run
會發現無法正常在 d1 的環境編譯,下一篇我們會對原始碼做一些修正讓他可以順利編譯給 D1 Mini 這塊開發板。