第一次看到 HAL(Hardware Abstraction Layer)是在 mruby/c 的原始碼裡面,我們可以透過編譯時提供 -DMRBC_USE_HAL_ESP32
來選擇要使用 ESP32 相關的硬體設置,而這些設置可能會影響如何做 STDOUT 或者其他處理。
看到這邊,上一篇對 mrb_puts
的實作似乎也是這樣沒錯
void mrb_puts(mrb_state* mrb) {
int argc = mrb_get_argc(mrb);
mrb_value* argv = mrb_get_argv(mrb);
#if defined(ESP8266)
char num[4];
tft.fillScreen(TFT_BLACK);
#endif
for(int i = 0; i < argc; i++) {
printf("%d\n", mrb_fixnum(argv[i]));
#if defined(ESP8266)
sprintf(num, "%d", mrb_fixnum(argv[i]));
tft.drawString(num, 8, i * 16 + 8, 1);
#endif
}
不同的是 mrubyc 是以檔案區分的,因此我們可以看到原始碼中會有 hal_esp32
、hal_posix
等目錄,如果我們可以有這樣的配置,是不是就能將桌機跟開發版的程式碼實作區分開來了呢?
在經過一些嘗試之後,因為 Arduino 需要的 setup
和 loop
跟桌機使用的 main
差異,所以最後只能採取將原始碼完全分離的處理,不過我們的使用情境大多會落在 Arduino 的狀況下,之後只需要區分 Arduino 跟桌機的測試版,似乎也還在接受範圍內,理想狀況下我們應該是透過不同的 Header 定義將 API 分離開來,讓我們可以呼叫相同的方法但能夠針對不同硬體做出對應的處理。
下面是嘗試調整後的結構
├── include
│ ├── embed_methods.h
│ ├── hal.h
│ └── ruby.h
└── src
├── hal_arduino
│ ├── embed_methods.cpp
│ └── hal.h
├── hal_native
│ ├── embed_methods.c
│ └── hal.h
└── main.cpp
首先我們先製作 include/hal.h
讓我們可以依照硬體類型選擇對應的 Header 作為參考使用。
#ifndef HAL_
#define HAL_
#if defined(ESP8266)
#include "hal_arduino/hal.h"
#else
#include "hal_native/hal.h"
#endif
#endif
然後再將 mrb_puts
定義到 include/embed_methods.h
方便其他實作可以參考到這個方法。
#ifndef EMBED_METHODS_
#define EMBED_METHODS_
#include <stdio.h>
#include "vm.h"
#include "iron.h"
#ifdef __cplusplus
extern "C" {
#endif
void mrb_puts(mrb_state* mrb);
#ifdef __cplusplus
}
#endif
#endif
另外因為 run_vm
也是共用的,所以改為 include/ruby.h
方便不同硬體呼叫。
#ifndef RUBY_
#define RUBY_
#include "app.h"
#include "vm.h"
#include "iron.h"
void bootstrap_ruby() {
mrb_state* mrb = mrb_open();
mrb_define_method(mrb, "puts", mrb_puts);
mrb_run(mrb, app);
mrb_close(mrb);
}
#endif
這邊可能還有其他更好的做法,不過因為是初次嘗試所以就先用相對單純的方式實現。
然後我們再根據不同的硬體時做對應的行為,像是 src/hal_arduino/hal.h
#ifndef ARDUINO_HAL_
#define ARDUINO_HAL_
#include <Arduino.h>
#include <TFT_eSPI.h>
#include "embed_methods.h"
static TFT_eSPI tft = TFT_eSPI();
#ifdef __cplusplus
extern "C" {
#endif
void setup();
void loop();
#ifdef __cplusplus
}
#endif
#endif
以及實作本身的 src/hal_arduino/embed_methods.cpp
#if defined(ESP8266)
#include "hal.h"
#include "ruby.h"
void mrb_puts(mrb_state* mrb) {
int argc = mrb_get_argc(mrb);
mrb_value* argv = mrb_get_argv(mrb);
char num[4];
tft.fillScreen(TFT_BLACK);
for(int i = 0; i < argc; i++) {
sprintf(num, "%d", mrb_fixnum(argv[i]));
tft.drawString(num, 8, i * 16 + 8, 1);
}
}
void setup() {
Serial.begin(9600);
tft.init();
tft.setTextSize(1);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
bootstrap_ruby();
}
void loop() {
}
#endif
因為 Arduino 的程式碼是以 C++ 為基礎,因此我們會需要用 cpp
作為副檔名,否則會無法編譯。
然後再針對桌機的版本實作 src/hal_native/hal.h
#ifndef NATIVE_HAL_
#define NATIVE_HAL_
#include "embed_methods.h"
int main(int argc, char** argv);
#endif
以及實際上的 mrb_puts
方法到 src/hal_native/embed_methods.c
#if !defined(ESP8266)
#include "hal.h"
#include "ruby.h"
void mrb_puts(mrb_state* mrb) {
int argc = mrb_get_argc(mrb);
mrb_value* argv = mrb_get_argv(mrb);
for(int i = 0; i < argc; i++) {
printf("%d\n", mrb_fixnum(argv[i]));
}
}
int main(int argc, char** argv) {
bootstrap_ruby();
return 0;
}
#endif
最後我們的 src/main.cpp
就會剩下一個基本上是空的檔案
/**
* iron-ruby-vm
*/
#include "hal.h"
雖然感覺很微妙而且應該還能有改進的空間,不過我們還是先往下一階段前進吧!