iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 21
0
IoT

拿到錘子的我想在微控制器上面執行 Ruby系列 第 21

Day 21 - 用 HAL 區分不同硬體

第一次看到 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_esp32hal_posix 等目錄,如果我們可以有這樣的配置,是不是就能將桌機跟開發版的程式碼實作區分開來了呢?

嘗試

在經過一些嘗試之後,因為 Arduino 需要的 setuploop 跟桌機使用的 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"

雖然感覺很微妙而且應該還能有改進的空間,不過我們還是先往下一階段前進吧!


上一篇
Day 20 - TFT 螢幕設定
下一篇
Day 22 - 比較處理
系列文
拿到錘子的我想在微控制器上面執行 Ruby30

尚未有邦友留言

立即登入留言