.NET対応組み込みデバイス「Netduino」入門(15)
Netduinoでロボットコントロール(後編) ― 16個のサーボモーターを一度に制御する
連載最終回。前回に引き続き、Netduinoを使ってPLEN.Dを制御してみる。今回は16個のサーボモーターを制御できるボードを使って複雑なロボットの動きを実現する。
PLEN.DはPLEN Projectが開発しDMM.make ROBOTSから2015年5月に発売が開始された全長25cmの運動神経抜群な組み立て式のロボットだ。全部で18軸のサーボモーターを搭載し、PWM信号△で制御している。
前回はそのモーターの2つをNetduinoに接続して制御してみたが、今回は合計16個のサーボモーターを一度に制御してみたいと思う。
PWM制御ボードについて
Adafruit 16-Channel 12-bit PWM/Servo Shield
Adafruit 16-Channel 12-bit PWM/Servo Shield(図1)は、Arduinoの上に乗せて(このようなボードをシールドと呼ぶ)I2Cで接続して使うボードだ。全部で16個のサーボモーターをI2Cインターフェースで制御でき、また、サーボモーターの動作用電源もArduinoからではなく専用の電源端子から供給できるようになっている。

I2Cなので別々のI2Cアドレスを設定することで、最大62個のAdafruit 16-Channel 12-bit PWM/Servo Shield同時使用が可能で、この場合は、全部で992個のサーボモーターを一度に制御する事すら可能になる。
ピンのはんだ付け箇所は多いが、一度作ってしまえば余計な配線もなくNetduinoと接続できる。
Adafruit 16-Channel 12-bit PWM/Servo Driver
シールドとしてNetduinoに直接乗せなくてもいいのであれば、もっと小型のAdafruit 16-Channel 12-bit PWM/Servo Driver(図2)も使い勝手がいい。
最大接続数の性能やソースコードもAdafruit 16-Channel 12-bit PWM/Servo Shieldと同じなので好みで選択してもいいだろう。

I2Cコマンドについて
それでは、今回のシールド/ドライバーの制御用コントローラーであるPCA9685のデータシートから使い方をひもといていこう。
PWMは一定の周期で5Vと0Vを繰り返す。この繰り返し間隔をPWM周波数といい、制御する対象によって決まってくる。今回は60Hzの動作周波数としたい。このような設定をしたいときに使うコマンドが、PWM周波数設定コマンドだ。
PWM周波数設定コマンド
制御バイトは10進数で254(16進数でFE)となり、PWM出力周波数のプリスケーラー(=分周回路)へのコマンドを読み書きするための、PRE_SCALEという名前のレジスタを意味している。PRE_SCALEレジスタへのコマンド書き込みは、MODE1レジスタ(=10進数/16進数で0/00)のSLEEPモードがオン(※後述)の場合にはブロックされる。
 コマンドバイトへの設定値は、設定したい周波数fから次の式で計算する。
![設定値=ROUND(25MHz/(4069×f[Hz]))-1](https:///re.buildinsider.net/small/netduino/15/math.gif)
例えば、60Hzであれば設定値は100となる。
周波数設定コマンドを投入するためには、Mode register 1(=MODE1レジスタ)のSLEEPモードを設定してから、周波数設定コマンドを(PRE_SCALEレジスタに)投入し、次にSLEEPモードを解除する。
| 
 '' レジスタ番号 
Private Const PCA9685_MODE1 As Byte = &H0 
Private Const PCA9685_PRESCALE As Byte = &HFE 
……中略…… 
public void SetPWMFreq (float freq) 
{ 
  Dim prescaleval As Single = 25000000  '' オシレーターのクロック周波数 
  prescaleval /= 4096 
  prescaleval /= freq 
  Dim prescale = CType(prescaleval - 1, Byte) 
  WriteToRegister(PCA9685_MODE1, &H31)         '' SLEEPモードオン 
  WriteToRegister(PCA9685_PRESCALE, prescale)  '' PWM出力周波数の設定 
  WriteToRegister(PCA9685_MODE1, &H21)         '' SLEEPモードオフ 
  ……中略…… 
} 
 | 
| 
 // レジスタ番号 
private const byte PCA9685_MODE1 = 0x0; 
private const byte PCA9685_PRESCALE = 0xFE; 
……中略…… 
Public Sub SetPWMFreq(freq As Single) 
  float prescaleval = 25000000;       // オシレーターのクロック周波数 
  prescaleval /= 4096; 
  prescaleval /= freq; 
  var prescale = (byte)(prescaleval - 1); 
  WriteToRegister(PCA9685_MODE1, 0x31);         // SLEEPモードオン 
  WriteToRegister(PCA9685_PRESCALE, prescale);  // PWM出力周波数の設定 
  WriteToRegister(PCA9685_MODE1, 0x21);         // SLEEPモードオフ 
  ……中略…… 
End Sub 
 | 
MODE1レジスタにおけるSLEEPモードのオン/オフは、4番ビットを2進数で01/00に設定することで指定できる。16進数で31はこのbitがオン、21はオフとなる。なお、いずれも0番ビットと5番ビットをオンにしているので注意してほしい。これらのビットの意味はデータシートを参照されたい。
WriteToRegisterメソッドは、制御バイトとコマンドバイトの2bytes分を書き込むために独自に実装したもの。
PWM出力コマンド
今回使用している制御ボードでのPWM出力は12bitデータで、PWMのオンとオフのタイミングが指定できる。つまり、PWM周波数設定コマンドで指定した周波数を4096で分割したタイミングで制御が可能となる。例えば、オンタイミングを0として、オフタイミングを4096にすると、60HzのPWMが出力できる。
PWM出力コマンドのフォーマットは図4のとおりで、出力ピン(=16チャンネル分の16本がある)に応じたレジスタ番号の1byteを指定し、それに続いて2bytes(=12bitデータで、そのうち4bit分は使用しない)のPWMがオンになるタイミング、2bytesのオフになるタイミングを設定することで実現する。
注意点としては、各2bytes値が下位バイトから設定する点だろう。具体的なコードは次のようになる。
| 
 '' レジスタ番号 
Private Const LED0_ON_L As Byte = &H06  '' 先頭0番ピンのレジスタ番号。残りの15ピンは、+4ごとの番号になる 
……中略…… 
Public Sub SetPWM(num As Byte, pwmOn As UShort, pwmOff As UShort) 
  Dim bOn = BitConverter.GetBytes(pwmOn) 
  Dim bOff = BitConverter.GetBytes(pwmOff) 
  Dim data = New Byte() {CType(LED0_ON_L + 4 * num, Byte), bOn(0), bOn(1), bOff(0), bOff(1)} 
  Write(data) 
End Sub 
 | 
| 
 // レジスタ番号 
private const byte LED0_ON_L = 0x06;  // 先頭0番ピンのレジスタ番号。残りの15ピンは、+4ごとの番号になる 
……中略…… 
public void setPWM(byte num, ushort pwmOn, ushort pwmOff) 
{ 
  var bOn = BitConverter.GetBytes(pwmOn); 
  var bOff = BitConverter.GetBytes(pwmOff); 
  var data = new byte[] { (byte)(LED0_ON_L + 4 * num), bOn[0], bOn[1], bOff[0], bOff[1]  }; 
  Write(data); 
} 
 | 
サーボモーターSG90での動作確認
今回のシールドやドライバーは、サーボモーターの電源を外部から共有するようになっている。そこで、ブレッドボード用5V/3.3V電源ボード Micro-B版を使ってUSBから5Vを供給するように配線する。
シールドならNetduinoの上に設置すれば設置完了だ。ドライバーの場合は次のように配線を行う。
配線ができたならば、ゆっくりサーボモーターを動かすコードを実行してみよう。
| 
 Sub Main() 
  Do While (True) 
    Using pwm As New PWMServoLib() 
      pwm.SetPWMFreq(60) 
      For i As Short = 150 To 700 - 1 
        For pwmnum As Byte = 0 To 15 
          pwm.SetPWM(pwmnum, 0, i) 
        Next 
      Next 
      Thread.Sleep(500) 
      For i As Short = 700 To 150 - 1 Step -1 
        For pwmnum As Byte = 0 To 15 
          pwm.SetPWM(pwmnum, 0, i) 
        Next 
      Next 
    End Using 
  Loop 
End Sub 
 | 
| 
 public static void Main() 
{ 
  while (true) 
  { 
    using (var pwm = new PWMServoLib()) 
    { 
      pwm.SetPWMFreq(60); 
      for (ushort i = 150; i < 700; i++) 
      { 
        for (byte pwmnum = 0; pwmnum < 16; pwmnum++) 
        { 
          pwm.setPWM(pwmnum, 0, i); 
        } 
      } 
      Thread.Sleep(500); 
      for (ushort i = 700; i > 150; i--) 
      { 
        for (byte pwmnum = 0; pwmnum < 16; pwmnum++) 
        { 
          pwm.setPWM(pwmnum, 0, i); 
        } 
      } 
      Thread.Sleep(500); 
    } 
  } 
} 
 | 
【動画】SG90動作確認
PLEN.Dのサーボモーター構成
PLEN.Dは全部で18個のサーボモーターが使用されている。1番から18番までがどこの動作を担当しているかを一覧にすると次のようになる。
| サーボモーター番号 | 用途 | 
|---|---|
| 1 | 左腕(前後) | 
| 2 | 左足(ひねり) | 
| 3 | 左腕(左右) | 
| 4 | 左ひじ | 
| 5 | 左足(左右) | 
| 6 | 左足(上下) | 
| 7 | 左ひざ | 
| 8 | 左足首(上下) | 
| 9 | 左足首(左右) | 
| 10 | 右腕(前後) | 
| 11 | 右足(ひねり) | 
| 12 | 右腕(左右) | 
| 13 | 右ひじ | 
| 14 | 右足(左右) | 
| 15 | 右足(上下) | 
| 16 | 右ひざ | 
| 17 | 右足首(上下) | 
| 18 | 右足首(左右) | 
Netduinoとの接続
今回、18個のサーボモーターに対して16チャンネルのPWM出力を割り当てる。サーボモーターは動かさないときでもその位置を指定したPWM出力を与えていないと外力により位置が動いてしまう。その特性を考えると自重を支える下半身関連は全て制御下に置かざるを得ないので、比較的影響の少なそうなPLEN.Dの4番(左ひじ)と13番(右ひじ)以外は全て接続することにする。
PLEN.Dプログラミング
PLEN.Dだけではないがラジコン飛行機も含め、多数のサーボモーターを使う場合には、事前にセンタリング位置のばらつきを合わせる作業が必要になってくる。今回も、まずは「気を付け」で直立不動になるPWMパルス間隔を、SG90動作確認サンプルを使ってブレークポイントで止めながら調査した。ブレークポイントで止めれば変数値を確認できるので、Visual Studioのリモートデバッグ機能はとても便利である。
PWMパルス間隔が決まったら、左右の手を元気よく振らせてみよう。
| 
 Sub Main() 
  Dim pwmBase = New UShort() {380, 460, 460, 380, 380, 550, 430, 360, 390, 400, 330, 400, 440, 300, 390, 400} 
  Using pwm As New PWMServoLib() 
    pwm.SetPWMFreq(60) 
    ''直立 
    For index As Byte = 0 To 15 
      pwm.SetPWM(index, 0, pwmBase(index)) 
    Next 
    Thread.Sleep(1000) 
    '' 
    pwm.SetPWM(Byte.Parse(EnumBorn.LeftArmLR.ToString()), 0, 440) 
    pwm.SetPWM(Byte.Parse(EnumBorn.LeftArmLR.ToString()), 0, 350) 
    Do While (True) 
      pwm.SetPWM(Byte.Parse(EnumBorn.LeftArmLR.ToString()), 0, 220) 
      pwm.SetPWM(Byte.Parse(EnumBorn.RightArmFB.ToString()), 0, 220) 
      Thread.Sleep(500) 
      pwm.SetPWM(Byte.Parse(EnumBorn.LeftArmLR.ToString()), 0, 560) 
      pwm.SetPWM(Byte.Parse(EnumBorn.RightArmFB.ToString()), 0, 560) 
      Thread.Sleep(500) 
    Loop 
  End Using 
End Sub 
 | 
| 
 public static void Main() 
{ 
  var pwmBase = new ushort[] { 380, 460, 460, 380, 380, 550, 430, 360, 390, 400, 330, 400, 440, 300, 390, 400 }; 
  using (var pwm = new PWMServoLib()) 
  { 
    pwm.SetPWMFreq(60); 
    // 直立 
    for (byte index = 0; index < 16;index++) 
    { 
      pwm.setPWM(index, 0, pwmBase[index]); 
    } 
    Thread.Sleep(1000); 
    // 
    pwm.setPWM((byte)EnumBorn.LeftArmLR, 0, 440); 
    pwm.setPWM((byte)EnumBorn.RightArmLR, 0, 350); 
    while (true) 
    { 
      pwm.setPWM((byte)EnumBorn.LeftArmFB, 0, 220); 
      pwm.setPWM((byte)EnumBorn.RightArmFB, 0, 220); 
      Thread.Sleep(500); 
      pwm.setPWM((byte)EnumBorn.LeftArmFB, 0, 560); 
      pwm.setPWM((byte)EnumBorn.RightArmFB, 0, 560); 
      Thread.Sleep(500); 
    } 
  } 
} 
 | 
EnumBorn列挙体は表1の内容をそのままの順番で値として保持している(※サーボモーターの先頭1番は、ピン番号としては先頭0番に対応しており、つまり値としては0スタートになるので注意してほしい)。
【動画】PLEN.D動作確認
まとめ
Netduinoでのロボット制御はいかがだっただろうか。サイズ的に収納は難しかったが、例えば、Netduinoの代わりに.NET Micro Frameworkが動作するG30TH Module、Adafruit 16-Channel 12-bit PWM/Servo Driver、そしてbluetooth接続用にSparkFun Bluetooth Modemを組み合わせれば、PLEN.Dの.NET Micro Framework化も十分可能かもしれない。興味のある方はPLEN.Dを購入して挑戦してみるのもよいだろう。
Netduinoをはじめとする.NET Micro Framework動作機器は、センサーからの入力を一次加工してクラウドに送信したり、一時的にMicroSDカードに保存してbluetoothを使ってスマホ経由でクラウドに送信したりなど、IoT時代になってますます注目されつつある。現に、.NET Micro Frameworkの次の版ではUWP(Universal Windows Platform) APIの一部やAllJoynなどのWindows 10近接通信がサポートされる予定になっている。
Windows 10 IoTが登場し、.NET Micro Frameworkはどうなるのかと心配した時期もあったが、今後もWindows 10 IoTよりもさらに小さな組込み用として生き残っていくだろう。
IoT時代には、センサーデバイスのハード知識、ノイズ対策、電源回りの知識を持った人間が全体設計をすることで、システムの総合的パフォーマンスを向上させることができるだろう。私はソフトだけ、私はハードだけの縦割りではなく、お互いが相手の分野を理解できるようになるのが理想的だ。本連載がソフトウェア開発者を対象にハードを作る楽しさを念頭に置いて、ソフトもハードも取り上げてきたのにはそういった意図があった。もし、この連載をきっかけにそういった技術者がふえてくれたら本望である。
※以下では、本稿の前後を合わせて5回分(第12回~第16回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
13. littleBitsではんだ付けなしの電子工作を実現
磁石で電子回路をつないで電子工作が行えるlittleBits。Netduinoと組み合わせると、どのような電子工作が実現できるのか? その可能性を探る。
14. MESHとNetduinoでiOSデバイスとの連携を実現
SONYのスマートDIYキット「MESH」とは? さまざまな連携が実現可能なMESHを使って、NetduinoとiPadを連携をさせてみよう。
15. Netduinoでロボットコントロール(前編) ― PLEN.Dを直接動かす
組み立て式のロボット「PLEN.D」に使われているサーボモーターを、NetduinoのPWM出力で制御してみよう。