機能の追加: C 言語から書く

はじめに

高級言語であるところの mruby/c は,下図(右)のように,C 言語の上に作られている. 言い換えると,mruby/c で書かれたプログラムは C で書かれたプログラムのラッパーとなっている. その特徴ゆえに,mruby/c でクラスを定義したり,機能を追加する際には, C 言語側も編集が必要になることに注意されたい.

クラス作成の概要

mruby/c のクラスを作成する場合には,以下の手順を踏むことになる.

  • main/Kconfig.projbuild を編集し,make menuconfig に項目を追加する.
  • main/ 以下に機能を追加するために C 言語 (ESP-IDF) で書かれたプログラムを追加する.
  • main/main.c において,後述の作法に従って mruby/c プログラムを include する.
  • mrblib/loops/ 以下に mruby/c で書かれたプログラムを置く.ここで新たなクラスを定義する.
  • mrblib/loops/ 以下に追加したクラスを用いたメインプログラム (master.rb) を書く.

本演習で用いている iotex-mrubyc-esp32 は以下のようなディレクトリ構造をしており,上記の手順に従って修正を行うことになる.

$ tree -L 4 iotex-mrubyc-esp32/

  mrubyc-template-esp32/
  ├── Makefile
  ├── components
  │   └── mrubyc              mruby/c の VM のソースなど
..........(中略)...............
  ├── main
  │   ├ Kconfig.projbuild   make menuconfig に出すメニューの定義  ← このファイルを編集
  │   └ mrbc_esp32_led.c 機能追加のためプログラム (C 言語)  ← このファイルを追加
  │   └ mrbc_esp32_led.h 機能追加のためのプログラム (C のヘッダファイル)  ← このファイルを追加
..........(中略)...............
  │   └── main.c        メインプログラム (C 言語)  ← このファイルを編集
  └── mrblib
      ├── loops        メインプログラム置き場
      └── models             クラス置き場      ← ここにプログラムを置く.

手順

以下では,しまねソフト研究開発センターのチュートリアル資料ハンズ・オン - 3(Lチカ:発光ダイオード点滅) に基づいて,LED を点灯させる機能を 1 から実装することにする.

手順 1: main/Kconfig.projbuild の修正

図に示すように,main/Kconfig.projbuild に項目を増やすと,make menuconfig した時のメニューが増える. 新たにクラスを定義するときは,main/Kconfig.projbuild を編集し,そのクラスを使うか否かのメニューを出すようにしておくと良い.

ここで,LED を点灯させるための "config USE_ESP32_LED" を設定する.

$ vi main/Kconfig.projbuild

   config USE_ESP32_LED
     bool "USR ESP32 LED"
     default y
     help
         use LED function?

   .....(略).....

手順 2: C 言語のプログラムの作成

ここでは簡単のために,LED を光らせるためのプログラム (C 言語) を追加する. ファイル名は main/mrbc_esp32_led.c, main/mrbc_esp32_led.h とする.

なお,main/mrbc_esp32_led.c は,C 言語 (ESP-IDF) のサンプル の内容を関数化したものであることを確認しておくこと.

main/mrbc_esp32_led.h

#include "mrubyc.h"
void mrbc_mruby_esp32_led_gem_init(struct VM*);

main/mrbc_esp32_led.c

#include "mrbc_esp32_led.h"        //自分のヘッダファイルを読み込む
#include "driver/gpio.h"

static void c_gpio_init_output(mrb_vm *vm, mrb_value *v, int argc) {
  int pin = GET_INT_ARG(1);
  console_printf("init pin %d\n", pin);
  gpio_pad_select_gpio(pin);
  gpio_set_direction(pin, GPIO_MODE_OUTPUT);
}

static void c_gpio_set_level(mrb_vm *vm, mrb_value *v, int argc){
  int pin = GET_INT_ARG(1);
  int level = GET_INT_ARG(2);
  gpio_set_level(pin, level);
}

void mrbc_mruby_esp32_led_gem_init(struct VM* vm){
  //関数を mruby/c から呼べるようにする                                                               
  mrbc_define_method(0, mrbc_class_object, "gpio_init_output", c_gpio_init_output); // c_gpio_init_output は gpio_init_output という名前で呼べるようにする
  mrbc_define_method(0, mrbc_class_object, "gpio_set_level", c_gpio_set_level);     // c_gpio_set_level は gpio_set_level という名前で呼べるようにする
}

手順 3: main/main.c の修正

make menuconfig でチェックを入れたクラスを利用可能とするため,C 言語側の メインプログラム (main/main.c) を修正する.

以下では例として,LED クラスを追加する場合に必要となる修正箇所を示す. main/Kconfig.projbuild において "config USE_ESP32_LED" と記述したとすると, make menuconfig でその項目をチェックしたか否かの情報は変数 CONFIG_USE_ESP32_LED に保存されることになる (接頭子として CONFIG_ が付される). ifdef を用いて,チェックが入った場合にのみ読み込むよう設定する.

.........(前略).................

//*********************************************
// ENABLE LIBRARY written by C 
//*********************************************
#ifdef CONFIG_USE_ESP32_LED
#include "mrbc_esp32_led.h"      C 言語のヘッダファイル (main/mrbc_esp32_led.h) を読み込むための設定.
#endif
.........(中略).................

//*********************************************
// ENABLE CLASSES and MASTER files written by mruby/c
//
// #include "models/[replace with your file].h"
// #include "loops/[replace with your file].h"
//*********************************************
#ifdef CONFIG_USE_ESP32_LED 
#include "models/led.h"         mruby/c の LED クラス (mrblib/models/led.rb) を読み込むための設定. 拡張子は .h に変えておくこと.
#endif
.........(中略).................

/* 
  !!!! Add your function                            !!!!
  !!!! example: mrbc_mruby_esp32_XXXX_gem_init(0);  !!!!
*/
#ifdef CONFIG_USE_ESP32_LED 
printf("start LED (C)\n");
mrbc_mruby_esp32_led_gem_init(0);  C 言語で書かれた LED 用の初期化関数 (main/mrbc_esp32_led.c に含まれる) を実行
#endif
.........(中略).................

/*
  !!!! Add names of your ruby files                              !!!!
  !!!! example: mrbc_create_task( [replace with your task], 0 ); !!!!
*/
#ifdef CONFIG_USE_ESP32_LED 
printf("start LED (mruby/c class)\n");
mrbc_create_task( led, 0 );             mruby/c で書かれた LED クラス (mrblib/models/led.rb) を呼び出す設定
#endif
.........(中略).................
}

手順 4: クラスの定義

mrblib/models/ 以下にクラスを定義するための mruby/c プログラムを置く LED を点灯させるための LED クラスは例えば以下のように書ける.

class Led
  def initialize(pin)
    @pin = pin
    gpio_init_output(@pin)
    turn_off
  end

  def turn_on
    gpio_set_level(@pin, 1)
    puts "turned on"
  end

  def turn_off
    gpio_set_level(@pin, 0)
    puts "turned off"
  end
end

手順 5: main プログラムの作成

mrblib/loops/master.rb を以下のように作成する.ここでは,LED の点灯と消灯を turn_on, turn_off というメソッドにしている.

led = Led.new(13)

while true
  led.turn_on
  sleep 1

  led.turn_off
  sleep 1
end

実行

make menuconfig において LED クラスにチェックを入れ,make, make flash すれば LED が点灯することが確かめられるだろう.

$ make menuconfig

  [*] USR ESP32 LED

$ make 

$ make flash monitor

課題

C 言語 (ESP-IDF) に含まれる内蔵タッチセンサを mruby/c で使えるようにせよ (現在は,内蔵タッチセンサ用のコードは mruby/c for ESP32 ライブラリに含まれていない). 最終的にはタッチセンサに触れた時に LED が点灯するようにせよ.

準備 1: C 言語で書いてみる

まず,C 言語で内蔵タッチセンサを動かしてみる.

$ cd ~
$ cd ~/esp
$ cp -r esp-idf/examples/get-started/hello_world ./touchsensor
$ cd touchsensor/

メインプログラムを修正.これは ESP-IDF のサンプル (esp-idf/examples/peripherals/touch_pad_read/main/esp32/tp_read_main.c) を元にしている

$ rm main/hello_world_main.c 
$ vi main/main.c

  // Touch Pad Read Example

  #include <stdio.h>
  #include "freertos/FreeRTOS.h"
  #include "freertos/task.h"
  #include "driver/touch_pad.h"

  #define TOUCH_THRESH_NO_USE   (0)
  #define tp0 0

  void app_main(void)
  {
      uint16_t touch_value;

      // Initialize touch pad peripheral.
      // The default fsm mode is software trigger mode.
      touch_pad_init();

      // Set reference voltage for charging/discharging
      // In this case, the high reference valtage will be 2.7V - 1V = 1.7V
      // The low reference voltage will be 0.5
      // The larger the range, the larger the pulse count value.
      touch_pad_set_voltage(TOUCH_HVOLT_2V7, TOUCH_LVOLT_0V5, TOUCH_HVOLT_ATTEN_1V);
      touch_pad_config(tp0, TOUCH_THRESH_NO_USE);

      // Start task to read values sensed by pads
      while (1) {
        touch_pad_read(tp0, &touch_value);
        printf("T%d:[%4d] \n", tp0, touch_value);

        vTaskDelay(1000 / portTICK_PERIOD_MS);
      }
  }

コンパイルと実行.なお,動作確認をするためには, 実習ボードの GPIO 4 に,ジャンパーケーブルをさしておく必要がある (ESP32 マイコン のピン配置を見ると,GPIO4 が T0 (タッチセンサー 0) が割り当てられていることがわかる). 実行しながらジャンパーケーブルを触ると値が変化する.

$ make
$ make flash monitor

  ....(中略).....
  I (287) cpu_start: Starting scheduler on PRO CPU.
  I (0) cpu_start: Starting scheduler on APP CPU.
  T0:[1095] 
  T0:[1095] 
  T0:[1094] 
  T0:[ 197]     触れる
  T0:[ 185]     触れる
  T0:[ 166]     触れる
  T0:[1082] 
  T0:[1095] 

準備 2: mruby/c のライブラリ作成

プロジェクトの準備

$ git clone https://github.com/gfd-dennou-club/iotex-esp32-mrubyc.git touch_sensor
$ cd  touch_sensor

あとは,前述の手順 1-5 にしたがって,mruby/c から内蔵タッチセンサーを使えるようにすれば良い.

なお,main/mrbc_esp32_tp.c はおおよそ以下のように書けるはずである ("....." の部分は自分で埋めること).

#include "mrbc_esp32_tp.h"
#include "driver/touch_pad.h"

#define TOUCH_THRESH_NO_USE   (0)

static void c_tp_init(mrb_vm *vm, mrb_value *v, int argc) {
  int pin = GET_INT_ARG(1); //ピン番号を引数に                                                                   
  ......     //初期化                                                                                    
  ......
  ......
}

static void c_tp_read(mrb_vm *vm, mrb_value *v, int argc) {
  uint16_t touch_value;
  int pin = GET_INT_ARG(1);    //ピン番号を引数に                                                                
  ......                       //touch_pad_read 関数を使って値を読む                                     
  SET_INT_RETURN(touch_value); //戻り値                                                                          
}

void mrbc_mruby_esp32_tp_gem_init(struct VM* vm){
  //関数を mruby/c から呼べるようにする                                                                          
  // c_tp_init を tp_init, c_tp_read を tp_read として mruby/c から呼べるようにする              
  mrbc_define_method( ....... );
  mrbc_define_method( ....... );
}

参考