mruby/cでマルチタスク

前回まではマイコンや周辺機器の基礎的な使い方をみてきました。 最終回は、mruby/cの特長のひとつであるマルチタスク機能を利用するプロジェクトをつくります。

使用パーツ

  • 赤色LED
  • サーミスタ(103AT)
  • 抵抗器330Ω
  • 抵抗器10kΩ
  • ジャンパーピン
  • ブレッドボード

タスクとは

LinuxやWindowsで言うところの「スレッド」とほぼ同じ意味です。 OSがスレッドごとにCPU時間の割り当てをコントロールし、複数のスレッド(処理のまとまり)を同時的に進行させる機能またはその動作状態のことをマルチスレッドと呼びます。

RTOS(リアルタイムOS)が載っているマイコンでもOSがマルチタスクをコントロールしますが、mruby/cにはOSなしでマルチタスクを実現する仕組みが含まれており、このことによって省メモリでありながら実用性の高いファームウェアを開発しやすくなっています。

※本記事のプログラムはESPのリアルタイムOSを使用しています。ただし、マルチタスクはmruby/cの機能によって実現しています。

ブレッドボードに配線する

breadboard_multi_tasks

前回までのLED回路とサーミスタ回路を組み合わせたものです。

プログラムを書く

cd $HOME/esp
git clone https://github.com/hasumikin/mrubyc-template-esp32.git multi-tasks
cd multi-tasks

まずは、前回のプロジェクト(taking-temperature)と同様に MRBC_USE_MATH を有効にしましょう。

main/main.c

#include "driver/gpio.h"
#include "driver/adc.h"
#include "esp_adc_cal.h"

#include "mrubyc.h"

#include "models/thermistor.h"
#include "models/led.h"
#include "loops/master.h"
#include "loops/slave.h"

#define DEFAULT_VREF    1100
#define NO_OF_SAMPLES   64
#define MEMORY_SIZE (1024*40)

static esp_adc_cal_characteristics_t *adc_chars;
static const adc_channel_t channel = ADC_CHANNEL_0; //GPIO4
static const adc_atten_t atten = ADC_ATTEN_DB_11;
static const adc_unit_t unit = ADC_UNIT_2;

static uint8_t memory_pool[MEMORY_SIZE];

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_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);
}

static void c_init_adc(mrb_vm *vm, mrb_value *v, int argc){
  adc2_config_channel_atten((adc2_channel_t)channel, atten);
  adc_chars = calloc(1, sizeof(esp_adc_cal_characteristics_t));
  esp_adc_cal_characterize(unit, atten, ADC_WIDTH_BIT_12, DEFAULT_VREF, adc_chars);
}

static void c_read_adc(mrb_vm *vm, mrb_value *v, int argc){
  uint32_t adc_reading = 0;
  for (int i = 0; i < NO_OF_SAMPLES; i++) {
    int raw;
    adc2_get_raw((adc2_channel_t)channel, ADC_WIDTH_BIT_12, &raw);
    adc_reading += raw;
  }
  adc_reading /= NO_OF_SAMPLES;
  uint32_t millivolts = esp_adc_cal_raw_to_voltage(adc_reading, adc_chars);
  SET_INT_RETURN(millivolts);
}

void app_main(void) {
  mrbc_init(memory_pool, MEMORY_SIZE);

  mrbc_define_method(0, mrbc_class_object, "gpio_init_output", c_gpio_init_output);
  mrbc_define_method(0, mrbc_class_object, "gpio_set_level", c_gpio_set_level);
  mrbc_define_method(0, mrbc_class_object, "init_adc", c_init_adc);
  mrbc_define_method(0, mrbc_class_object, "read_adc", c_read_adc);

  mrbc_create_task( thermistor, 0 );
  mrbc_create_task( led, 0 );
  mrbc_create_task( master, 0 );
  mrbc_create_task( slave, 0 );
  mrbc_run();
}

mrblib/loops/master.rb

$status = "COLD"

led = Led.new(19)

while true
  case $status
  when "COLD"
    # do nothing
  when "HOT"
   led.turn_on
   sleep 0.1
   led.turn_off
  end
   sleep 0.1
end

mrblib/loops/slave.rb

thermistor = Thermistor.new

while true
  temperature = thermistor.temperature
  puts "temperature: #{temperature}"
  $status = if temperature > 30
    "HOT"
  else
    "COLD"
  end
  sleep 1
end

mrblib/models/thermistor.rb

B = 3435
To = 25
V = 3300 # mV
Rref = 10_000 # Ohm

class Thermistor
  def initialize
    gpio_init_output(0)
    gpio_set_level(0, 1)
    init_adc
  end

  def temperature
    vref = read_adc
    r = (V - vref).to_f / (vref.to_f/ Rref)
    1.to_f / ( 1.to_f / B * Math.log(r / Rref) + 1.to_f / (To + 273) ) - 273
  end
end

mrblib/models/led.rb

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

解説

うまく実行できたでしょうか? サーミスタを指で触って温度が30℃を超えると、LEDが点滅します。

capture_multi_tasks

今回のプロジェクトには2つの無限ループ(master.rbとslave.rb)があり、それらがグローバル変数 $status を通して連携しています。

このように複数のタスクがユーザからの入力を待ち受けたり、表示器をコントロールしたり、ネットワークの接続状況やリクエストを監視したりして相互に連携するのが、ファームウェア開発の面白さです。 mruby/cを使えばこのようなマルチタスクを容易につくることができ、さらにRubyという言語の高い生産性を組み合わせられることがおわかりいただけたのではないかと思います。

本記事はこれにて終了です。お付き合いありがとうございました。