LED通信事業プロジェクト エンジニアブログ

F#でラズパイを動かす(5)

F#で「Lチカ」 Part.2

記事更新日 2022年9月6日


はじめに

小さくて安い教育用コンピューターであるRaspberry Pi通称ラズパイですが、このラズパイがここまで広まることとなった最大の理由はプログラムによって電圧を制御することができる物理ピンを装備していることです。この物理ピンによって高度な「電子工作」が可能になります。 ちょっと時間が空いてしまいましたが、今回は前回から引き続き、F#でLチカする方法を説明していきます。今回は、まず最初にそのソースコード全体を載せて、その後中身を個別に説明したいと思います。

今回は、LEDONOFFという名前のプロジェクトで動かします。別にプロジェクト名は何でもよいので各自自由につけて頂ければと思いますが、この記事ではLEDONOFFというプロジェクト名であるという前提で説明をしていきますのでご承知置きを。

ソースコード全体

既存のソリューションを開き、ソリューションエクスプローラーでソリューションを選択し右クリック、そこから[追加]-[新しいプロジェクト]を選んでLEDONOFFという名前の「F#コンソールアプリ」を作成してください。プロジェクトを作成するとprogram.fsというファイルができていると思いますが、その中身をすべて消して下のコードを貼り付けて下さい。これで準備は完了です。

/// program.fs ソースコード open System open System.Device.Gpio /// LEDをON/OFFする関数 let ledonoffmain() = // GPIOの宣言 let gpio = new GpioController() // GPIO Port番号の宣言 let gpioPinNo = 14 //GPIOのピン番号(物理ピン番号ではない) /// 実際にLEDをON/OFFするための関数。 /// GPIOピン番号<int> -> 読んだキー<ConsoleKeyInfo> -> 終了かどうか?<bool> let ledonoff pinNo readChr = // GPIOがOpenしているか判断してからピンオープン(エラー処理付き) if gpio.IsPinOpen pinNo = false then gpio.OpenPin(pinNo, PinMode.Output) //ピンオープン Console.WriteLine(" Port " + pinNo.ToString() + " Open") else () // 入力手文字によって動作を変化させるmatch式 match readChr.ToString() with | "0" -> // OFFにする gpio.Write(pinNo, PinValue.Low) //LED OFF false //続ける | "1" -> // ONにする gpio.Write(pinNo, PinValue.High) //LED ON false //続ける | "e" -> true //終了 | _ -> false //その他は無視で続ける /// プログラムの中心となるプロシージャ let mainprocedure = Console.WriteLine("点灯「1」, 消灯「0」, 終了「e」") //表示用メッセージ // 繰り返すための再帰関数 let rec readString() = let readKeyinfo = Console.ReadKey() let result = ledonoff gpioPinNo (readKeyinfo.KeyChar) //ON/OFF関数を実行 if result = false then readString() //もう一回実行 else () //終了 do readString() //再帰関数実行 // 最終的にGPIOピンを閉じる(エラー処理付き) if gpio.IsPinOpen gpioPinNo = true then gpio.ClosePin(gpioPinNo) // GPIOピンを閉じる Console.WriteLine(" Port " + gpioPinNo.ToString() + " Close") else () 0 //ledonoffmain関数全体の戻り値(コマンドプロンプトの戻り値である0を戻す) [<EntryPoint>] let test args = ledonoffmain() //上の関数を実行(戻り値0)

このコードはコマンドプロンプトで「1を押すとLEDが点灯、0を押すと消灯、eを押すと終了」というとても単純なプログラムですが、一応F#ならではのコードを2つほど入れつつ書いてみましたので、それを含めて、各行をおおよそ実行される順に説明していきたいと思います。

各行の説明

準備:NuGetを使ってGPIOライブラリをインストール

各行の説明の前に、一つ重要な準備がありますのでそれを説明します。

ラズパイの電子ピンには様々な機能がありますが、多くのピンが対応している最も重要な機能がGPIOをいうものです。GPIOは”General-purpose input/output”の略で、デジタル信号の出力と入力が出来る機能を指します。ラズパイにある沢山のピンは出力にも入力にも使うことができます。出力を使えば、LEDと点灯させたり、モーターを動かせたりしますし、入力を使えば、ボタンやスイッチの切り替えを検出したり、パルス信号を読み取ることができます。GPIOはラズパイの代名詞的な機能とも言えます。当然今回のLチカもGPIOを使います。

Lチカで使うのはGPIOの入出力のうち出力の方だけですが、とにもかくにもGPIOが操作できなければ始まりませんので、F#(.NET)でGPIOを操作できるようにする必要があります。GPIOは.NET標準の状態では動きませんので、インターネットから.NETでGPIOを操作するための機能(ライブラリ)をインストールする必要があります。.NETでプログラミングしたことのない人だと「インターネットからライブラリをインストールとは敷居が高い」と思われるかも知れません。しかし、.NETであれば超簡単です。ブラウザを開く必要もコマンドを打つ必要もありません。NuGetを使えばいいのです。

NuGetは、.NETで使うことのできる様々な追加機能、すなわりライブラリを集めた場所です。そこにはMicrosoftが作ったライブラリだけでなく、ユーザーが作ったライブラリも集められています。そして、NuGetを使えば何も考えることなくボタン一つで好きなライブラリをインストールしてくれます。どんだけ簡単かは今回のGPIOのインストール手順を見て頂ければ理解頂けると思いますので、まずはインストール手順を説明しましょう。

  1. ソリューションエクスプローラーでプロジェクト"LEDONOFF"を右クリックして、Nuget参照の管理を選ぶ
  2. 開いたNuGet管理画面で参照タグを選ぶ
  3. 検索窓に"gpio"と入力
  4. System.Device.Gpioを選んで、右側のウインドウの"インストール"を押す。(バージョンは最新(2.10)でOK)
fig.1
図1:NuGet管理画面

これだけです。これで、LEDONOFFというプロジェクトでGPIOが使えるようになりました。どうです?NuGet凄いと思いませんか?検索すれば分かりますが、GPIOに限らず、様々なライブラリがNuGetを通して追加できるようになっています。また、GPIOではありませんでしたが、もしあるライブラリを追加するのに他に必要なライブラリがある場合、それも自動でインストールしてくれます。また、アップデートもアンインストールもボタン一つです。まあ、実際のところここだけで必要な機能を探すというのはちょっと難しいですが、それでもここまで簡単に新しい機能が追加できる、というのも.NETの魅力の一つです。

どのライブラリが追加されてるかを見たい場合は、ソリューションエクスプローラーのプロジェクトにある[依存関係]-[パッケージ]を開けば分かります。.NET6のF#だと、標準でFSharp.Coreが入っています。そして今回System.Device.Gpioを追加したため、正しくインストールされていればそれも入っているはずです。

Open宣言

open System open System.Device.Gpio

最初に説明するのは、実際の動作とは直接関係の無い、この二行です。

F#でライブラリを呼び出して何らかの関数(やクラス)を使いたい場合、その関数を呼び出すためにその関数の書いてある場所、つまりライブラリの場所を指定する必要があります。「関数が納められている住所」と言い換えても良いかも知れません。標準のものも、追加したものも.NETで使われるライブラリには必ず住所がつけられています。この住所の事を.NETではネームスペース(namespace)と呼んでいます。

標準以外の関数を呼び出すときはそのネームスペースから書く必要があります。例えばコマンドプロンプト(コンソール)に入出力するために用いるConsole(クラス)の場合、それが含まれるネームスペースはSystemなので、Consoleを使った関数やメソッドを呼び出すときには

// 完全修飾サンプル System.Console.Writeline("メッセージ")

の様に書く必要があります。さらにGPIOの場合だと"System.Device.Gpio"というネームスペースのため、例えばGPIOを操作するオブジェクト宣言をするのに

// 完全修飾サンプル let gpio = new System.Device.Gpio.GpioController()

と長々と書く必要があります。この様に関数を住所から全て書くことを「完全修飾」と呼びます。「完全修飾」は住所で言えば差し詰め「都道府県から」完全な住所を書くことと言えます。まあ、ただ完全な住所といってもこの1行だけでしか使わないのであればたいしたことありませんが、普通は同じネームスペースにある関数を何度も使います。同じ都道府県レベルからの住所がコード中に何度も出てくるわけです。そうなると、コードは冗長になり可読性が落ちる・・・ それに加えて、コード全体でいろいろなネームスペースを参照した場合、そのコードがどのネームスペースを使っているのかを知るにはコードを全て追わないといけない事になります。後からコードを流用する場合、そのコード内でなんのネームスペースを使っているかを知らないとエラーが発生しかねません。

そういったことを防ぐため、一番最初(上)に「このネームスペースを使いますよ」という宣言をしてしまって、その後のコードはネームスペース名の入力を省略できるようにしてしまいます。これをOpen宣言と呼んでいて、例えばOpen宣言を使うと上のコードはこの様なコードになります。

// open宣言のサンプル open System.Device.Gpio let gpio = new GpioController()

Open宣言は通常コードの一番上の方に書くため、それを見ればそのコードで何のネームスペース(つまりライブラリ)を使っているのかわかります。また、Open宣言は住所で言えば「都道府県市町村名」まで宣言したのと同じなので、後のコードでは地区名だけ書けば良くなりますので、コードをすっきり短くすることができます。また、このOpen宣言ですが、ライブラリだけで無く同一ソリューション内にある別のプロジェクトを参照するときにも使えます。

今回は、Systemという外部からのインストール不要の標準ネームスペースと、NuGetで追加したSystem.Device.Gpioを使っています。尚、このOpen宣言と同じ機能はC#やVB.NETにもあって、C#ならusing、VB.NETならimportでネームスペースを宣言することができます。Open宣言は、.NET言語において一番重要なルールの一つと言えるでしょう。

最初に呼び出される関数

[<EntryPoint>] let test args = ledonoffmain() //上の関数を実行(戻り値0)

前回説明したとおり、[<EntryPoint>]と宣言された関数が、一番最初に呼び出される関数です。このtest関数は、実際にLEDをON/OFFするための関数であるledonoffmainという関数を呼び出しているだけでそれ以外の中身はありません。そして、こちらも前回説明しましたが、コマンドプロンプトを呼び出す関数の戻り値は必ず整数(int)でなくてはならないため、ledonoffmain関数は0が戻り値になるように書かれています(また後で説明します)。

ledonoffmainで最初に実行される関数

/// LEDをON/OFFする関数 let ledonoffmain() = // GPIOの宣言 let gpio = new GpioController() // GPIOピン番号の宣言 let gpioPinNo = 18 //GPIOのピン番号(物理ピン番号ではない) ・・・・

let ledonoffmain() = は、大きな関数の宣言です。Lチカするためのコードはこの関数の中に含まれています。F#において関数の区切り(どこからどこまでか)を表すのはタブによるインデントです。次の行からは必ずインデントを一つ下げて下さい。

この関数の上にはコメントが付いています。F#のコメントは、C#等多くの言語と同じように行頭に//をつけるのですが、ここでは///とスラッシュを三本つけています。これはVisual Studioのルールで、関数の上に///でコメントをつけるとそれがその関数のドキュメントとして表示されるようになります。関数にマウスオーバーすると///で入れたコメント付きで関数の説明が表示されます。言葉で言うと分かりづらいですが、こんな風にです。

fig.1
図2:トリプルスラッシュによるポップアップ

これは便利な機能ですので、関数のコメントにはできるだけ///をつけるようにしましょう※1。特に、繰り返し使う様な関数や、ファイルを跨いで使う関数などの場合は必須です。

さて戻りまして、ledonoffmain関数で最初に実行されるのは、引数を持たない下の二行です。(F#では引数を持たない関数は上から順次自動的に実行されますが、引数を持つ関数は別の関数から呼び出されないと実行されません。)

GpioControllerはGPIOのオブジェクトを表します。つまり、これを宣言するとgpioという変数(オブジェクト)はラズパイのGPIOピン全体を表すものとなります。以降はGPIOピンに対する操作はこのgpioというオブジェクトに対して行っていきます。これはF#が「関数型言語でありながら.NETとしてのオブジェクト指向言語の性質も持つ」ことを利用したものです。

次の行のlet gpioPinNo = 14というのは使用するピン番号の設定ですが、これはGPIOのピン番号を表します。ラズパイのピン番号はラズパイの物理的なピン番号とGPIOに設定されているピン番号の二つがあります。どの物理ピンがどのGPIOピンにあたるのかは、今更このページで掲載せずともいろいろなサイトに掲載されていますが、ここは一応F#の記事ですので本家本元であるMicrosoft公式サイトをリンクしておきます。この公式ページの図でページのグレーの四角で囲ってある数字が「物理的なピン番号」、オレンジで「GPIO xx」と書かれているのが、GPIOのピン番号となります。ここで使うのは後者の「GPIO xx」という番号のほうです。サンプルコードではGPIO 18番を使っていますが、物理ピンでは12番にあたります。結構勘違いしやすいので、お気をつけ下さい。

GPIOはどのピンでも同じ動作ができますので、Lチカもどのピンでも動作します。ですから、コード中のピン番号は変更しても全く問題ありません。ただし、ピン番号そのものは何度も呼び出す値のため、コードの真ん中でピン番号を指定するより、こうやって最初の方に宣言して定数にしておいた方が便利です。(他の言語ならピン番号をStaticで宣言をするところですが、F#は全ての変数が最初からStaticなので・・・)

メインとなるプロシージャ

/// プログラムの中心となるプロシージャ let mainprocedure = Console.WriteLine("点灯「1」, 消灯「0」, 終了「e」") //表示用メッセージ ・・・・

ledonoffmain()の内、次に実行されるのはメインとなるプロシージャです。mainprocedureと名付けています。この中は、LEDを操作する(点灯・消灯)ためのコードではなく、主にユーザーの操作を受け付けるためのコードが記載されています。ユーザーの入力によって動作が変わりますが、その「動作」自体は別の関数にしてあります。

最初の1行は、操作を説明し、ユーザーに入力を求めるメッセージです。このメッセージ自体はあってもなくても構いません。LEDをON/OFFする操作は次のように設定します。「1を押すと点灯、0を押すと消灯、eを押すと終了」です。

再帰関数

// 繰り返すための再帰関数 let rec readString() = let readKeyinfo = Console.ReadKey() //キー入力を取得 let result = ledonoff gpioPinNo (readKeyinfo.KeyChar) //ON/OFF関数を実行 if result = false then readString() //もう一回実行 else () //終了 do readString() //再帰関数実行

さて、今回の肝となる二つのF#っぽい文法のうちの一つ「再帰関数」です。再帰関数は「関数の中で自分自身を呼び出す関数」のことをいい、同じ操作を繰り返すような時に使います。今回なら、「入力を待つ」=>「入力内容にしたがって判断」という処理を(無限に)繰り返すわけですが、これを再帰関数で表現します。F#では、letの後にrecを入れると再帰関数を意味することになります。再帰を英語で言うと"recursive"なのでその頭文字です。

さて、再帰関数の中身ですが、中身の上2行から見ていきます。この2行は再帰させて繰り返し実行させたい内容です。

let readKeyinfo = Console.ReadKey()は、コマンドプロンプトのキー入力を読み取る行です。ReadKey()メソッドが実行されると、コマンドプロンプト上でキーが入力されるまで待ちます。何らかのキーが押された場合、ConsoleKeyInfoという型の戻り値が返されます。

このConsoleKeyInfo型は扱いがちょっと面倒で、そのままだとなんのキーが押されたのか分からない状態になっています。今回の例では次行でKeyCharプロパティを使って押されたキー情報を一旦Char型に変換しています。Char型は「1文字」格納するための変数(2バイト)ですが、F#だとChar型も扱いにくい※2ので、後でさらに文字列に変換して使います。

let result = ledonoff gpioPinNo (readKeyinfo.KeyChar)は、上の方にあるLedonoffと言う関数を呼び出しています。この関数は押したキーによってLEDをONにしたりOFFにしたりする関数ですが、具体的な中身は後程説明します。この関数の戻り値は「終了キー(e)が押されたかどうか」のbool値です。上のReadKey()の際に"e"が押されていたらTrueが返ってきます。それ以外のキー操作ではFalseが返ってきます。

つまり

  • 押されたキー情報を受け取る
  • キーによって処理を変える関数に投げる

という2つの処理を繰り返すのがこの関数の中身ということになります。

// 再帰するかどうかを判断するif文 if result = false then readString() //もう一回実行 else () //終了

次の3行は、再帰するか、外に出るかを判断するためのif文です。先ほど説明した「終了かどうか(="e"を押したかどうか)の戻り値」であったbool値resultがFalseの場合は操作を「まだ繰り返す」ことを意味します。そのため、自分自身である関数readString()を呼び出します。これにより、もう一回readString()関数が呼び出す、つまり再帰します。一方、resulttrueだった場合、この関数は終了となるため、何もしないという意味でunitを返しています。readString関数はここで終わるため、戻り値としてはunitを取ることになります。

ちなみに、F#において"="は代入を意味しないため比較演算子は"="です。他言語のように比較と代入と区別するために"=="を使う必要はありません。

この様に、再帰関数というのは、途中で「もう一度自分自身を呼び直す」関数を言います。何度も自分を呼び直すため、同じ処理が繰り返し行われます。また、この例では引数がunitの関数でしたが、意味のある引数を取った関数で再帰関数を組む事もできます。例えばこんな関数です。

// 引数のある再帰関数のサンプル let rec test1 counter = if counter < 1000 then //ここになんかの処理 test1 (counter + 1) //+1して再帰する else () //再帰関数からでる do test1 0 //再帰関数実行

これはtest1という再帰関数を1000回実施する関数です。再帰する際に引数を1ずつ足していって最終的に1000になったら再帰関数から出ます、という関数です。これで、繰り返し一定回数処理を行うという関数ができます。

「繰り返し一定回数処理を行う」と言われたら、他の言語メインの方ならfor文やdo while文を思い浮かべますよね?そうです。実際、再帰関数とforやdo文でやれることは大して変わりません。そもそもF#でもfor文は使えます(do while文はないです)。じゃあ、なんでわざわざ再帰関数を使っているのか?それはF#だから。正直に言えば再帰関数のメリットはもっと複雑な条件分岐がないと発揮されません。だから今回の例のような通常の処理なら、再帰関数じゃなくてfor文で書いたって良いんです。しかし、やっぱり関数型言語であるF#としては繰り返し処理だって関数化したい!だから式の複雑さにかかわらず再帰関数を極力使う様にしています。慣れれば、for文よりもシンプルになるかも?

GPIOのピンオープン

// GPIOがOpenしているか判断してからピンオープン(エラー処理付き) if gpio.IsPinOpen pinNo = false then gpio.OpenPin(pinNo, PinMode.Output) //ピンオープン Console.WriteLine(" Port " + pinNo.ToString() + " Open") else ()

GPIOのピンを操作する前に、まず「開く」処理をする必要があります。GPIOのピンを開くにはgpio.OpenPin(ピン番号, ピンモード)というメソッドを実行します。ピンモードは入力か出力のいずれかです。今回は出力なのでPinMode.OutPutをしています。このメソッドは一つ問題があって、すでに開かれているピンをもう一度開こうとするとエラーになる、ということです。普通に処理されていればピンは閉じられているものですが、このプログラムが何らかの理由でエラーで終わってしまったときにピンが開いている可能性もあるため「念のため」エラー回避処理を入れています。IsPinOpneはピンが開いているかどうか問い合わせる関数ですので、それがfalseだったときだけピンを開くようにしています。

LEDの点灯とmatch式

// 入力手文字によって動作を変化させるmatch式 match readChr.ToString() with | "0" -> // OFFにする gpio.Write(pinNo, PinValue.Low) //LED OFF false //続ける | "1" -> // ONにする gpio.Write(pinNo, PinValue.High) //LED ON false //続ける | "e" -> true //終了 | _ -> false //その他は無視で続ける

ここの文が、LEDをON/OFFさせる本体となります。ここにはF#っぽい文法のもう一つ「match式」が使われています。match式は条件分岐の式なのですが、この条件を様々なパターンで記述することができ、他の言語(の似たような式)よりも非常に強力なものとなっております。例えば、条件分岐のためのパターンを別の関数にもたせる(アクティブパターン)こともできます。今回は、単純に入力したキーによって操作を変えるといういちばんシンプルな条件分岐式です。引数であるreadChrChar型のため、ToString()メソッドをつけて文字列に変換しています。そのため、条件式は文字列での比較になっています。文字列である"0"と"1"がそれぞれOFF/ONに対応、"e"は終了です。match式における”その他”は"_"(アンダーバー)になっていて、今回は「何もしない(なんの処理もしない)」となっています。そして、match式の戻り値はbool型になっていて、終了以外はfalseを返し、終了の場合のみtrueを返すようになっています。このmatch式の戻り値がledonoff関数の戻り値になっており、この関数を呼び出した側で終了(つまり"e")を押したかどうか分かるようになっています。今回の例では、再帰関数からこの関数を呼び出していて、戻りがfalseであるかぎり再帰して繰り返し、戻りがtrueとなると再帰から出る形になっています。

さてmatch式の中身、すなわち処理の話ですが、"0""1"の条件内にあるのはgpio.Write(Pin番号, 電圧)というメソッドです。このメソッドは、指定したGPIOのピンに対してピンの電圧(High/Low)のいずれか選ぶというものです。PinValueをHighにすれば電圧がかかる(+3.3V)ためLED点灯、LowにすればLED消灯となっています。つまり、LEDをON/OFFさせる本質の部分はこの1行だけなんです。(ただし、事前にピンをオープンしてないとエラーになりますが・・・)

ただそれだけ?と思った方がいると思います。そうです、GPIOのOutputモードでは、結局のところピン電圧のON/OFFしかできないのです。しかも、電圧は低く、使える電力も小さいためLEDを1個光らせることぐらいしかできません。しかし、その代わりアプリによって自由に、かつ高速に電圧のON/OFFを切り替えることができます。この高速なON/OFF切り替えを上手く使えば、例えばモーターの速度を制御したり※3、通信したり※4することが可能です。

終了処理

・・・・ // 最終的にGPIOピンを閉じる(エラー処理付き) if gpio.IsPinOpen gpioPinNo = true then gpio.ClosePin(gpioPinNo) // GPIOピンを閉じる Console.WriteLine(" Port " + gpioPinNo.ToString() + " Close") else () 0 //ledonoffmain関数全体の戻り値(コマンドプロンプトの戻り値である0を戻す)

これは終了処理です。GPIOピンを閉じています。Closeしないと大きな問題があるわけではありませんが、リソースを開けるという面でもCloseはしておきましょう。そして、Close後は最初のtest関数、すなわちコマンドプロンプトの関数※5のために0を返しています。

おわりに

比較的シンプルなLチカのコードを、あえてF#っぽく書いてみたのがこのソースコードです。Lチカシリーズ最終回の次回は、LEDと抵抗など物理的な接続と、実行の仕方などを説明していきます。


※1; C#やVB.NETは、さらにタグをつけてXML構造にすると、引数や戻り値まできれいに表示してくれるようになるが、F#には残念ながらその機能がない。

※2; F#は型にとても厳密なため、C#やVB.NETと同じようにChar型を扱えない。例えばC#であれば自動的に文字列型(String)へキャストしてくれるが、F#ではキャストは行われない。

※3; デューティー比でモーター速度を変えることをPWM(Pulse Width Modulation)と呼ぶ。GPIOでソフトウエア的にPWMを作成するだけでなく、ラズパイではハードウエア的にPWMを作ることもでき、一般にそちらの方が正確にモーターを制御することができる。.NETではその機能をSystem.Device.Pwmネームスペースから使用することができる。

※4; ラズパイの物理ピンでの通信はGPIO2,3番に割り振られているI2C(Inter-Integrated Circuit)ポートで行うのが一般的。I2Cに対応したセンサー類が数多くある。.NETではその機能をSystem.Device.I2cネームスペースから使用することができる。

※5; 前回で説明したとおり、コマンドプロンプトを呼び出す関数はint型の値を返さなければならない。その中で、0は「正常終了」を意味するため、通常は0を返すようにする。