ハンズ・オン - 5
※この記事に掲載されている情報は、2019年6月時点のものです。
mruby/cでマルチタスク
前回まではマイコンや周辺機器の基礎的な使い方をみてきました。 最終回は、mruby/cの特長のひとつであるマルチタスク機能を利用するプロジェクトをつくります。
使用パーツ
- 赤色LED
- サーミスタ(103AT)
- 抵抗器330Ω
- 抵抗器10kΩ
- ジャンパーピン
- ブレッドボード
タスクとは
LinuxやWindowsで言うところの「スレッド」とほぼ同じ意味です。 OSがスレッドごとにCPU時間の割り当てをコントロールし、複数のスレッド(処理のまとまり)を同時的に進行させる機能またはその動作状態のことをマルチスレッドと呼びます。
RTOS(リアルタイムOS)が載っているマイコンでもOSがマルチタスクをコントロールしますが、mruby/cにはOSなしでマルチタスクを実現する仕組みが含まれており、このことによって省メモリでありながら実用性の高いファームウェアを開発しやすくなっています。
※本記事のプログラムはESPのリアルタイムOSを使用しています。ただし、マルチタスクはmruby/cの機能によって実現しています。
ブレッドボードに配線する
前回までの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が点滅します。
今回のプロジェクトには2つの無限ループ(master.rbとslave.rb)があり、それらがグローバル変数 $status
を通して連携しています。
このように複数のタスクがユーザからの入力を待ち受けたり、表示器をコントロールしたり、ネットワークの接続状況やリクエストを監視したりして相互に連携するのが、ファームウェア開発の面白さです。 mruby/cを使えばこのようなマルチタスクを容易につくることができ、さらにRubyという言語の高い生産性を組み合わせられることがおわかりいただけたのではないかと思います。
本記事はこれにて終了です。お付き合いありがとうございました。