Showing posts with label 実験. Show all posts
Showing posts with label 実験. Show all posts

7/20/2012

9DOF Sensor Stickをmbedでテスト



Sparkfunから出ている9軸モーションセンサを試してみたので備忘録を記しておきます.このセンサはわずか1x3.5cm ほどの基盤に3軸ジャイロ,3軸加速度センサ,3軸地磁気センサを搭載していて,価格も$100程度とお手頃なボードです.接続も,I2Cインタフェースなので電源2本,信号2本のわずか4本です.

mbedでのテスト環境は右図のように超簡単です.mbedのVOUT(3.3v regulated), GND, p9(SDA)とp10(SCL)に直結するだけ.普通I2Cはオープンコレクタなのでプルアップ抵抗が必要ですが,このセンサは内部で3.3Vにプルアップされているので必要ありません.

mbedのCookbookセクションにサンプルコードがあるので,これを若干の手直しするだけですぐ試せます.
  • 3軸加速度センサ ADXL345
    サンプルからADXL345_I2C.h, ADXL345_I2C.cpp, main.cpp を持ってくる.main.cpp で ADXL345_I2C accelerometer(p9, p10); のピン番号とシリアル通信の速度 pc.baud(115200); の部分を確認するだけ.
  • ジャイロ ITG-3200
    サンプルからITG3200.h, ITG3200.cpp, main.cpp を持ってくる.ITG3200.h でI2Cのアドレスを0x68に修正.
               #define ITG3200_I2C_ADDRESS 0x68 //7-bit address.
    
    main.cpp で ITG3200 gyro(p9, p10); のピン番号とシリアル通信の速度 pc.baud(115200); の部分を確認するだけ.
  • 地磁気センサ HMC-5883L
    サンプルからHMC5843.h, HMC5843.cpp, main.cpp を持ってくる.これは旧機種のHMC5843用のものなのでデータシートに従って微修正.まずHMC5843.h でY軸とZ軸のレジスタ番号を入れ替える.
    /* exchange Y<->Z */
    #define HMC5843_Z_MSB        0x05
    #define HMC5843_Z_LSB        0x06
    #define HMC5843_Y_MSB        0x07
    #define HMC5843_Y_LSB        0x08
    
    あとは同様にmain.cpp で HMC5843 compass(p9, p10); のピン番号とシリアル通信の速度 pc.baud(115200); の部分を確認するだけ.
 Arduinoのシリアルモニタで手軽に確認.問題なし.
Thanks to: 北卓人君

7/01/2012

歌うモータ

私の居室は,薄い壁一枚を隔てて学生の実験室と隣接しています(正確には,実験室の一角を壁で仕切って教員室にしているという感じです).話し声はさほど聞こえないのですが,モータなどの機械音だけはよく伝わってきます.私にとっては騒音どころかBGMみたいなものです.

一番大きいのは工作機械の音ですが,毎日聞いているとどの機械でどんな加工をしているか大体わかります.「糸鋸でアクリル切ってるな」とか,「そんな削り方じゃフライスの刃をいためるなあ」とか.音だけでも加工の上手下手は結構わかりますが,ほとんどは材料の固定のしかたと刃の送り方で決まるものです.

また,誰かが実験を始めるとロボットの音が聞こえてきます.それぞれ個性的な動作音をもっていますし,使っているモータの種類やギヤ比も違うので,誰がどのロボットを動かしているのか一耳瞭然です.たまにいつもと違う音が聞こえてくると,「あ,誰か新しいメカを作ったのかな?」などと思って,イソイソと実験室を覗きに行ってしまいます.

さて,このようにモータの音というのはなかなかに個性的なものですが,これを活用(悪用?)すると,こんな余興も可能になります.
用意するものはごくありふれたモータ制御系一式.ここで挙げたのはマイコン(Arduino Pro mini 3.3V),モータドライバTB6612FNG,タミヤのミニモータ,Li-po電池(3.7V),そして紙コップです.30分もあれば組めるようなものです.ここでマイコンにプログラムを書きこめば普通にモータを動かせるわけですが,そこにちょっと細工をすると…

  

DCモータをPWM制御すると,「ピー」という特有の甲高い音がしますよね.あの音はPWMのキャリア周波数に起因しています.普通はモータの時定数などを考慮して適当に決めてそのまま一定にしておくのですが,その周波数をオンラインでどんどん切り替えてしまえば,このように音階を奏でることができるというわけです.

いろいろ試してみたところ,モータ単体では蚊の鳴くような微弱な音しか鳴りませんが,紙コップの底に貼りつけるとかなりはっきり聞こえる音になります.
※動画ではギアボックスがついていますが,ギアボックスをつけずにモータを直接紙コップに貼り付けた方がきれいに聞こえることがわかりました.

音楽のスタンダードでは基準音階であるA4が440Hz,周波数が2倍になるごとに1オクターブ=12半音階上がりますので,周波数を2^(1/12)倍にすれば半音上がることになります.なおPWM制御ではデューティ比によってモータの速度を変えるわけですが,試してみた結果,聞こえる音はほぼ周波数だけで決まり,デューティ比にはほとんど関係がありませんでした.従って,ここではモータが動かないようにデューティ比を5%とごく小さく設定しています.

結局この現象は磁界中のコイルを振動させて音を出していることになるので,実はダイナミックスピーカの仕組みそのものです.モータとスピーカが同じ電磁誘導という原理に基づいていること,また振動源だけではだめで,適切な形状のコーンによる拡声が重要なこと,などを説明するのによい教材になるのではと思っています.


追記:こんな動画もありました.同じようなことを考える人はいるものです.

8/01/2011

PWM制御あれこれ







前回の記事では,Cortex-M0ボードの演算速度を調べるためにPWM出力端子の波形をいろいろとみていたわけですが,今回はその出力をモータドライバに通したときに気になる特性をいくつか確認してみました.(従って,主役はマイコンではなくてモータドライバの方です.)

まずはライブラリとしてCQ.zipで配布のpwm.cをベースにしますので,搬送波の生成に32ビットタイマTMR32B1,デューティー比の設定にマッチレジスタMR0, MR1を使うことになります.モータドライバとしては,以前紹介したTB6612FNGを使います.

【TB6612FNGのスイッチング性能】

TB6612FNGのデータシートをみますと,スイッチングの周波数は最大100kHzとあります.すなわち10μsecくらいの間隔のスイッチング性能は保証されていることになりますが,実際のところはどの程度なのでしょうか.

pwm.cでは32ビットタイマTMR32B1が下記のように設定されています.
#define MAX_PWM 999

[...]

    LPC_TMR32B1->PR  = 0; 
    LPC_TMR32B1->MR2 = MAX_PWM;
    LPC_TMR32B1->MCR = (0x3 << 6); // MR2で割込み&リセット
TMR32B1はシステムクロックの48MHz(すなわち20.83nsec/クロック)をプリスケーラで分周してカウントします.ここではプリスケーラの設定が0(すなわち1分周)ですので,TMR32B1のカウント周期もそのまま20.83nsecになります.そして,レジスタMR2との一致で割込みおよびリセットをするように設定されていますので,カウント値が MR2に設定した値MAX_PWMに達するまでの時間がPWMの搬送波周期です.1カウント20.83nsecで0〜999までカウント,すなわち 20.83μsecというわけです.さらに,設定したデューティー比によってこの20.83μsecの時間がON区間とOFF区間に2分されます.

モータドライバの最小スイッチング周期が10μsecということですから,スペック的には限界ぎりぎりでOKということになるでしょう.実際,モータドライバの入力(黄色)と出力(水色)を比較してみると次のようになります.
横軸は時間で一目盛りが5μsecで,2本の垂直カーソルで挟まれた区間が搬送波一周期分の20.8μsecです.出力はぴったり入力に追従できていますので,まったく問題なさそうです.

調子に乗ってカタログスペックを超え,PWMの搬送波周期をもっと短くしてみます.
 図の左上から順に約10μsec(時間目盛り2.5μsec/div で表示), 約4μsec (1μsec/div), 約1μsec (250nsec/div) と設定した場合です.時間目盛りを細かくしていくと,スイッチングに250nsec程度の遅れがあることが見えてきます.実用的なのはせいぜい10μsec周期くらいまででしょうか.しかしまあ,思ったよりもずっと応答が俊敏であることがわかりました.

【マイコンとモータの電源系統の話】
基本的なことですが,マイコンとモータドライバおよびモータは下図のように接続します.

Vccはマイコンやモータなどの弱電回路用の電源,Vmはモータ駆動用の電源です(GNDは共通).つまり2系統の電源が必要になります.それで,よく学生などに「VccとVmは一緒じゃダメなんですか?」ということを聞かれるので,なぜダメなのかを少し見てみましょう.

左はVccとVmを別にした場合,右は共通にした場合です.いずれも黄色はVcc,水色はモータ端子の片方(OUT1)をはかったものです.Vccを供給しているバッテリの電圧は4.1Vほどで,モータはマブチの130系という小さなものです.左図ではそのまま一定になっているのに対し,右図ではかなりのノイズが乗っているうえ,モータの動作に合わせて480mVほどの電圧降下を起こしてしまっています.

影響の度合いはバッテリーの駆動能力と負荷の大きさにもよりますが,やはりVccとVmは分けておくのが原則です.




7/29/2011

Cortex-M0ボードでリアルタイム実数演算






Cortex-M0ボード"MARY"で実数演算を使ったリアルタイム制御がどの程度できるのかを確認していきます.
まずやってみたのはこれです.

【サンプル1: 浮動小数点演算で正弦波出力】
#ifdef __USE_CMSIS
#include "LPC11xx.h"
#endif

#include "pwm.h"
#include "math.h"
#define omega (2 * 3.141592 / 10.0)
int sample_out = 0; 
uint32_t msec;
float u;

int main(void)
{
  sample_out = 0;
  GPIOSetDir(1, 8, 1);          // PIO1_8 : OUTPUT

  Init_PWM();

  // Set TIMER32B0 as Free-Running milliseconds Timer
  LPC_SYSCON->SYSAHBCLKCTRL |= (1<<9);    // Timer32B0 Clock ON (48MHz)
  LPC_TMR32B0->PR  = 48000;    // Pre-scaler : 48MHz/48000 → 1msecで1カウント
  LPC_TMR32B0->TCR = 1;        // Timer Start

  while(1)
    {
      GPIOSetValue(1, 8, sample_out); 

      msec = LPC_TMR32B0->TC; 

      u = sin(omega * msec);   /* -1 〜 1 */
      PWM_Set_Duty(0, (uint16_t)((u+1)*500) ) ; /* 0 〜 1000 */
      sample_out = 1 - sample_out;
    }
    return 0; 
}
このプログラムは32ビットタイマとしてTMR32B0とTMR32B1の二つを使用します.TMR32B0は1kHzのフリーランニングタイマとして用います.カウント値(TMR32B0TC)は現在時刻[msec]を表し,これを変数msecに読み込みます.一方,TMR32B1はPWM信号の生成に用います.ここではCQ.zipのサンプルにあるpwm.cを利用していますので,デフォルトで搬送波周期が約20usec,0〜999の整数でデューティー比を設定できます.これらを用い,msec を浮動小数点演算ライブラリ math.h の sin 関数に与えることにより,100Hz(周期10msec)の正弦波をPWM出力します.

下図のように,PWMの出力(CN1の8番,青)とサンプル周期テスト出力(CN3の1番,黄色)をオシロで測ってみましょう.


結果は以下のとおり.
黄色がサンプル周期出力,青がPWM出力です.少し見づらいですが,サンプル周期は420usecくらい(計算負荷によって変動します).PWMの方はデューティー比を周期的に変化させているので疎密波のようにみえます.これを単純なRC回路(R=33kΩ,C=0.1μF)で平滑化してみてやると,
のように,確かに周期10msecの正弦波が出ていることがわかります.

以前に調べた限界のサンプル周期は2usecほどでしたので,sin一つをいれただけでずいぶん時間がかかるようになったのがわかります.また,コードサイズは 21KB ほどで,メモリが32KBしかないことを考えるとこれもかなりの大きさです.

【サンプル2: 固定小数点演算で正弦波出力】
CQ.zipで配布されているfixedpoint.hを使ってsinの部分を固定小数点演算に書き換えてみます.
#ifdef __USE_CMSIS
#include "LPC11xx.h"
#endif

#include "pwm.h"
#include "fixedpoint.h"
int sample_out = 0; 
uint32_t msec;

int main(void)
{
  fix32_t u, omega; 
  int32_t duty;

  omega = FIX_Mul(FIX_PI, FIX_DBL(0.2));  // omega = 2*PI/10 (100Hz)

  sample_out = 0;
  GPIOSetDir(1, 8, 1);          // PIO1_8 : OUTPUT

  Init_PWM();

  // Set TIMER32B0 as Free-Running milliseconds Timer
  LPC_SYSCON->SYSAHBCLKCTRL |= (1<<9);    // Timer32B0 Clock ON (48MHz)
  LPC_TMR32B0->PR  = 48000;    // Pre-scaler : 48MHz/48000 → 1msecで1カウント
  LPC_TMR32B0->TCR = 1;        // Timer Start

  while(1)
    {
      GPIOSetValue(1, 8, sample_out); 

      msec = LPC_TMR32B0->TC; 

      u = FIX_Sin( FIX_Mul(omega, FIX_INT(msec)) );     /* -1 〜 1 */
      duty = INT_FIX( FIX_Mul(u, FIX_INT(500)) ) + 500; /* 0 〜 1000 */
      PWM_Set_Duty(0, duty) ; 
      sample_out = 1 - sample_out;
    }
    return 0; 
}
すると結果は
のように,サンプル周期は220usecと約半分になりました.また,コードサイズは約10KBと,こちらも約半分です.劇的とまではいえませんが,うまく使えば効果的でしょう.

7/22/2011

Cortex-M0ボード:ハードの基礎・マニュアルの読み方





学生へのチュートリアルと備忘録を兼ねてメモしておきます.

Cortex-M0ボードに搭載・接続されている様々なハードウェアを使用するためのやり方は,先に紹介したトラ技2011年4月増刊や本誌連載(2〜8月号)にわかりやすく解説されています.しかし,ちょっと自分で何かやろうとするとすぐにこれでは情報が足りないことに気づくでしょう.著者自身も書いているように,ちゃんと理解するにはデータシートやマニュアル,そしてソースファイルを読むことが何よりも近道です.

【例】
  • ポートPIO_1_8 (汎用IOポート1の8番端子)からON/OFF信号を出力したいとします.増刊号のp.50, あるいは配布ファイルCQ.zipの LPC1114/hardware/LPC1114_MARY_DesignData.pdf を参照すると,PIO1_8はMARYボードのCN3の1番ピンに割り当てられていることがわかります.また表Aの右の方を見ると,OB, LB, XB, GB, UB などの拡張ボードではすでに別の用途に使用されているので,PIO1_8を使うときはこれらのボードを外さなければならないこともわかります. この端子からON信号を出力するには以下のようにします.
    #Ifdef __USE_CMSIS
    #include "LPC11xx.h"
    #endif
    
    #include "gpio.h"
    
    int main(void)
    {
        GPIOSetDir(1, 8, 1);   // PIO1_8 : OUTPUT
        GPIOSetValue(1, 8, 1);   // ON
        while(1) {}
        return 0; 
    }
    

    最初の3行は,まずはオマジナイだと思って下さい.汎用入出力のための関数のヘッダファイルが gpio.h で,この中で GPIOSetDir や GPIOSetValue が宣言されています.GPIOSetDir で信号の入出力方向を「出力」に設定し,GPIOSetValueで ON や OFF の値をセットするわけです.

  • 次に,同じことをポートPIO_1_0についてやろうとしてみます.PIO_1_0はMARYボードのCN4の6番ピンです.GPIOSet...
    の部分を
        GPIOSetDir(1, 0, 1);   // PIO1_0 : OUTPUT
        GPIOSetValue(1, 0, 1);   // ON
    
    と変えれば十分のように思いますが,これだけではうまくいきません.

    その理由は,p.50表Bの「端子番号7」と「22」のところを比べてみるとわかります.ここでいう端子番号とはMARYボードのピン番号とは別で,ボード上にみえる8mm角ほどのマイコンチップ本体,LPC1114の端子番号の事です.このLPC1114のような小型のマイコンチップでは,サイズを節約するために一つのピンに複数を機能を割り当てていることが多いのです.端子7はPIO1_8/CT16B1_CAP0の2種類兼用,端子22はPIO1_0/TMS/CT32B1_CAP0/AD1の4種類兼用というように.端子を使うときには,このうちどの役割をさせるのかを指定しないといけないわけです.表Bの2列目をみると端子機能のデフォルト設定が書いてありますね.端子7はデフォルト(リセット直後)でPIO1_8になっているのでたまたま設定の必要がなかったのですが,端子22はReservedとなっているので明示的に設定しないといけないわけです.

    端子22をPIO1_0として機能させるためには,上記2行の前に
        LPC_IOCON->R_PIO1_0   = 0xd1; // PIO1_0 -> PIO1_0
    の一行を追加する必要があります.

【マニュアルの読み方】
こういった情報はどこを調べればよいのでしょうか.そのためにはまず,このMARYボードシステムの基本的な成り立ちを理解する必要があります.

名称開発元マニュアル
1. CPUコアCortex-M0ARMCortex-M0テクニカルリファレンスマニュアル (ARM DDI0432CJ)
2. マイコンチップ(MCU)LPC1114NXPセミコンダクタLPC111x/LPC11C1xユーザーマニュアル (NXP UM10398)
3. マイコンボードMARYCQ出版・圓山宗智氏トラ技増刊号,CQ.zip に附属のドキュメント他
包含関係はCortex-M0 ⊂ LPC1114 ⊂ MARY です(※).いわゆるCPUはCortex-M0ですが,これは表には見えていません.見えているのはLPC1114で,これはCortex-M0をコアとしてタイマやA/Dコンバータといった周辺機能を1チップにまとめたものです.MARYボードはCQ出版・圓山氏が今回の企画で開発したもので,LPC1114を中心に電源,USBインタフェース,コネクタ・スイッチ・LEDなどがついて完成したシステムになっています.

なにか調べようとしたとき,それが3つのうちどれに関係しているのか,を考えれば参照すべきマニュアルがわかるでしょう.基本的な演算能力や命令セットについて知りたければCortex-M0のマニュアル(1)を見ます.タイマ,I/Oポート,A/D変換,UART(シリアル入出力) などの周辺機能関連についてはすべて LPC1114の マニュアル(2)を見ることになりますので,おそらく最も参照頻度が高いと思われます.LPC1114のある端子がどのコネクタに出ているかとか,電源関係,拡張ボードの使い方などに関することはすべて MARY のマターになります.さらにはライブラリ,ヘッダファイル,サンプルプログラムなどのソフトウェアもCQ出版・圓山氏の作によるものなので,トラ技の記事やCQ.zipに附属ドキュメント(3)に頼ることになります.

さて,上の例の場合は汎用入出力GPIOの設定の問題ですので,LPC111xユーザーズマニュアルを開きます.第6章のTable 80を見ますと,PIO1_0端子の機能を設定するレジスタ IOCON_R_PIO1_0 の設定方法の説明が出ています.端子機能を PIO1_0 にセットするには第2〜0ビットを 001 にします.その他の第7〜3ビットはデフォルトのままで 11010 としますので,あわせて 11010001B = 0xd1 をセットすればよいというわけです.

※当然ですが,Cortex-M0を使ったマイコンチップはLPC1114以外にもありますし,LPC1114を使ったボードも MARY以外にもたくさんあります.また,Cortex-M0の命令セットはThumbといって,他のいくつかのCPUにも共通して使われています.


【追記:C言語からのアクセス】
LPC1114のユーザーマニュアルを見て周辺機能レジスタの詳細を理解したとして,実際にそれにどうやってアクセスするかを確認するには LPC1114/workspace/CMSISv1p30_LPC11xx/inc/LPC11xx.h を参照するとわかりやすいです.例えば上述したIOCON_R_PIO1_0にアクセスしたいと思ったら,LPC11xx.hの中を検索すると
/*------------- Pin Connect Block (IOCON) --------------------------------*/
/** @addtogroup LPC11xx_IOCON LPC11xx I/O Configuration Block 
  @{
*/
typedef struct
{

 ...(中略)...

  __IO uint32_t R_PIO1_0;               /*!< Offset: 0x078 I/O configuration for pin TMS/PIO1_0/AD1/CT32B1_CAP0 (R/W) */

 ...(中略)...

} LPC_IOCON_TypeDef;
/*@}*/ /* end of group LPC11xx_IOCON */
という構造体の定義部分が見つかります.さらにLPC_IOCON_TypeDefで検索すれば,その下の方に
#define LPC_IOCON             ((LPC_IOCON_TypeDef  *) LPC_IOCON_BASE )
とあって,構造体の実体がLPC_IOCONという名前で宣言されていることがわかります.それで,LPC_IOCON->R_PIO1_0と記述すれば構造体のメンバR_PIO1_0を読み書きできるというわけです.

※冒頭に書いたオマジナイは,このLPC11xx.hをインクルードするためのものです.

7/19/2011

ARM Cortex-M0ボード"MARY"のリアルタイム性能





CQ出版・マルツパーツから販売されているARM Cortex-M0基盤 "MARY" (トランジスタ技術2011年4月増刊に付属)は,超小型ながら必要十分な基本機能と高い拡張性をもった,とてもクリスプなマイコンです.ご多分にもれず私も3月の発売時に大人買いしました(単体で買っても1980円,増刊号では3000円で2枚付属するという破格の安さも魅力の一つです).
  • マイクロコントローラ:NXP LPC1114
  • CPUコア:ARM Cortex-M0
  • 動作クロック:48MHz
  • 内蔵フラッシュメモリ:32kBytes
まずは全開で回したとき(デジタル出力をON/OFFするだけのループ)の確認です.ループ周期は約1.7μsecでした. さすがに速いです.パルスにかなりヒゲが出ているのはGPIOの応答限界なのでしょうか.

ソース:
#Ifdef __USE_CMSIS
#include "LPC11xx.h"
#endif

int main(void)
{
  int sample_out; 

    GPIOSetDir(1, 8, 1);   // PIO1_8 : OUTPUT
    sample_out = 0;

    while(1)
    {
      GPIOSetValue(1, 8, sample_out); 
      sample_out = 1 - sample_out;
    }
    return 0; 
}



1000カウントの空for ループを入れて回したとき.ループ周期は約400μsecとなりました.



図は省略しますが,10000カウントにすると約4msec (=4000μsec) でした.デクリメントと条件分岐が一回あたり4μsecくらいかかるということですね.十分すぎるほど速いように見えますが,Cortex-M0は乗除算が(整数であっても)複数サイクルを要することと,浮動小数点演算はさらにさらに高コストなことを考えると,制御則などを下手にコーディングすると一気に時間がはねあがることも予想されます.これからもう少し演算性能をテストしておこうと思います.Cortex-M3,M4との比較も早くやってみたいところです.

追記:簡単な演算テストをしてみた結果は以下の通りでした.クロスコンパイラはgccです.
  •  int どうしの乗算 3.6μsec
  •  int どうしの除算 7μsec
  •  float どうしの乗算 60μsec
  •  float の sin関数 240μsec
コンパイラの最適化処理にも依存するので一概にはいえませんが,だいたいの目安にはなりそうです.やはりfloatで sqrt, sin, cos, exp などの数学関数を使うと急激に時間が増えます.二,三段の複合計算をしたら簡単に1msecを超えました.非線形制御を実装するときにはさまざまな工夫が必要です.

追記の追記:
Arduinoのリアルタイム性能についても少し追記しておきましたが,Arduinoでは全開で回したときの周期が460μsecでした.sinやcosを入れると数十msecまで増えることもあります.

7/11/2011

モータドライバ TB6612FNGの使い方

最近よく使っている小型のモータドライバTB6612FNGと使用上の注意点について記しておきます.
TA7291P

TB6612FNG
TB6612FNGはMOSFETを使用した低損失なデュアルモータドライバで,連続1.2A,ピーク3.2Aの電流が流せます(電圧は15Vまで).小型ロボットの製作にはぴったりです.
単体のデータシートはこちらです. サイズは表面実装のSSOPですが,SparkfunからBreakoutボード(写真左)が出ていて,スイッチサイエンス等で購入できます(単価は900円程度).

小型ロボットでは他にも,超安価(200円程度)で使いやすいバイポーラ型のTA7291P (写真右)などを使う人も多いと思いますが,この手のものは損失が大きく(=発熱が大きく)スイッチングが遅いのが難点です.TA7291Pが放熱板つきで連続1.0Aなのに対してTB6612FNGはこのサイズで連続1.2Aですから, 効率の違いがよくわかります.

さて,かように優れもののTB6612FNGですが,使い方を誤ると全然効果を発揮しません.モータAの入力端子には AIN1, AIN2, PWMAの3本があり,AIN1, AIN2 の組で方向を指定,PWMAにPWM制御パルスを加えるようにすると以下のようになります(以下はArduinoで制御).


黄色がPWM制御パルス (PWMA),水色がモータドライバ出力(A01)です(この場合A02は0Vで一定です).PWMの搬送波周期は4msです.水色がきちんと黄色を追従していることがわかります.

ところが,使い方を誤って(あるいはピン数をケチって)PWMAをHIGHに固定し,AIN1, AIN2の一方をLOWに,もう一方にPWM制御パルスを使ってしまうケースがみられます.この場合は以下のようになってしまいます.

 黄色がPWM制御パルス(AIN1),水色がモータドライバ出力(A01)です.期待した通りにスイッチングが行われていません.実は上の図はまだモータをつないでいないときの状況で,モータをつなぐとさらに酷くなります.
 

モータドライバの使い方になれていない人はご注意下さい.

※後ほどもう少し加筆

7/05/2011

ArduinoとPC(Processing)のシリアル通信:承前






先の記事でArduinoのリアルタイム性能の確認,およびシリアル通信におけるバイト通信のすすめについて述べました.最後に,実際にバイト通信をするための簡単なサンプルプログラムを挙げておきます.Arduinoで現在時刻,アナログ端子A0およびアナログ端子A1の値をそれぞれ取得し,シリアルでPCに送信するというものです.

基本動作:
  • 現在時刻(マイクロ秒)をmicros()で取得すると unsigned long intすなわち4バイトの値が返ってきますが,そのうち下位の1バイトを無視して上位の3バイトだけ送信しています.2^32[usec]≒4295[sec]ですので,71分くらいで飽和します(注:計算間違ってたので直しました).
  • アナログ信号をanalogRead()で取得するとintすなわち2バイトの値が返ってきますが,精度は10bit,つまり 0〜1023の整数値が返ってきます.
  • unsigned と signed の挙動の違いを確認するため,data[1]の方は512を引いて -512〜511 の整数値を送信しています.
  • 送信したいデータは時刻が3バイト,アナログ信号がそれぞれ2バイトで計7バイトになります.これを配列buffer[0]〜buffer[6]に格納します.
  • サンプル周期4[msec]で回しています.端子D12から確認用のパルスを出しています.

終端バイトとエスケープの設定:
  • 一回分の送信データの末尾に終端バイトDLMを付加することにします.ここでは0x80です.
  • もし buffer[0]〜buffer[6] にDLMと同じバイトが含まれていたら,別のバイトESCに置き換えます.ここでは0xFFです.
  • 置き換えを行ったら,buffer[7] の第0〜第6ビットの対応するところを1にして記録します.ここで,buffer[7]の第7ビットは使わないので,buffer[7]自身が DLM=0x80と一致することはあり得ません.
  • buffer[0]〜buffer[6]に,エスケープ情報buffer[7]と終端バイトbuffer[8]を加えて,計9バイトを送信します.

Arduino側プログラム:
const int analog_0 = 0;
const int analog_1 = 1;
const int sample_out = 12;
const long int dT = 4000;
const byte DLM = 0x80;
const byte ESC = 0xFF;

boolean sample_flag;
long int time_us;        /* current time [usec] */

void setup()
{
  pinMode(sample_out, OUTPUT);
  Serial.begin(57600);
  sample_flag = true;
}

void loop()
{
  int data[2];
  byte buffer[9];
  
  time_us = micros();
  
  /* data 0, 1 */
  data[0] = analogRead(0);
  data[1] = analogRead(1);

  digitalWrite(sample_out, sample_flag?HIGH:LOW);
  sample_flag = !sample_flag;
  
  buffer[0] = byte((time_us>> 8) & 0xFF);
  buffer[1] = byte((time_us>>16) & 0xFF);
  buffer[2] = byte((time_us>>24) & 0xFF);  
  buffer[3] = byte( data[0]      & 0xFF);
  buffer[4] = byte((data[0]>> 8) & 0xFF);
  buffer[5] = byte( data[1]      & 0xFF);
  buffer[6] = byte((data[1]>> 8) & 0xFF);
  
  buffer[7] = 0;
  for (int i=0; i<=6; i++) {
    if (buffer[i] == DLM) {  /* if the delimiter appears*/
      buffer[7] += 1<<i;
      buffer[i] = ESC;     /* escape */
    }
  }

  buffer[8] = DLM;         /* send delimiter */

  Serial.write(buffer,9);
  delayMicroseconds( max(time_us+dT-micros(), 0) );
}

Processing側では次のようにコーディングします.
  • Serial.bufferUntil(DLM)を使用し,終端バイトDLMを受け取ったら割り込みをかけるように設定します.コールバック関数はserialEvent()です.
  • Serial.readBytesUntil(DLM)を使用し,終端バイトDLMまでのデータを読み込みます.(余分なデータが残っていると負の数が返されます. )
  • あとは基本的にArduino側のプログラムと逆の操作です.エスケープbitが立っていた場合は当該バイトをDLMに差し替えます.
  • 【重要】以前にも書きましたが,Arduinoのint型が2バイトなのに対しProcessingのint型は4バイトです.元の2バイト整数が2の補数で正負表現されていたとしても,これを4バイト整数に代入すると正しく正負が表現されません.たとえば -1=0xFFFF が 0x0000FFFF = +65535 になってしまうわけです.そこで,0x8000以上の数値は負の数と解釈するように修正が必要です.

Processing側プログラム:
import processing.serial.*;

byte DLM = byte(0x80);
int[] data = new int[3];

void setup() 
{
  Serial myPort;

  size(320, 200);
  frameRate(30);
  
  println(Serial.list());
  String portName = Serial.list()[0];
  myPort = new Serial(this, portName, 57600);
  myPort.clear();
  myPort.bufferUntil(DLM);
}

void draw() { 
  background(0);
  fill(255);
  
  for (int i=0; i<data.length; i++) {
    textAlign(RIGHT);  text( nfc(data[i],0), 150, 50*i+50 );
    textAlign(LEFT);   text( ": data(" + nf(i,1) + ")", 170, 50*i+50 );
  }
}

void serialEvent(Serial port)
{
  byte[] buffer = new byte[9];

  if (port.readBytesUntil(byte(0x80),buffer) <0) { 
    port.clear(); 
  } else {

    for (int i=0; i<=6; i++) {              /* check the escape flag */
      if ( ((buffer[7]>>i) & 0x01) == 1) {
        buffer[i] = DLM;                    /* restore! */
      }
    }
    
    data[0] = (((buffer[2]<<24)&0xFF000000) | ((buffer[1]<<16)&0xFF0000) | ((buffer[0]<<8)&0xFF00));
    data[1] = ( ((buffer[4]<<8)&0xFF00) | (buffer[3] & 0xFF) );
    data[2] = ( ((buffer[6]<<8)&0xFF00) | (buffer[5] & 0xFF) );
    if (data[2] >= 0x8000)  data[2] -= 0x10000;   /* from unsigned to signed */
  }
}



以上,学生が通信でハマったときに「ここ見といて」で済ませられるようにTipsをまとめておきました.もっと洗練された書き方もできると思いますが,とりあえず挙動を理解するためのサンプルということでこれ以上は詰めていません.なにかお気づきの方がおられましたらメールでご指摘いただければ幸いです.

※メモ:Serial.print(val,BYTE)を使うとSerial.printでも生のバイトデータを送信できますが,送られるのは1バイトだけです.valがintやlongなどの多バイト型であっても,実際に送られるのは最下位の1バイトです.

7/01/2011

ArduinoとPC(Processing)のシリアル通信



こちらの続編です.Arduinoを使ったリアルタイム制御,とくに母艦PCとのシリアルデータ通信をしながら制御するときの注意点についてまとめています.

シリアル通信のAPI

シリアル通信のAPIをArduino側,PC側(ここではProcessing)のそれぞれについて整理しておきます.
この記事ではArduino→PCの方向の通信についてのみ示します.

Arduino側

Arduino IDEのSerialライブラリを使います.
  • Serial.print(val, format)
    val をASCII文字列としてシリアルポートに出力します. valは任意のデータ型でOKです.すなわちchar, byte, int, float, char[] (文字列)などが扱えますが,すべてASCII文字列に変換して出力されます.valが数値の場合はformatによって変換書式を指定します(省略可).Serial.print(123)のようにすると"123"という3バイトの文字列が送信されるということです.
  • Serial.write(val)
    valをそのままバイトデータとしてシリアルポートに出力します.val は byte型です.char, int などbyte型に暗黙キャストできるデータ型もOKですが,正負の扱いなどでハマるので明示的にbyteにキャストしておいたほうがいいです.複数バイトをまとめて送るには Serial.write(byte[] buf, int buf_len) のようにバイト配列を与えます.

Processing側

ProcessingのSerialライブラリを使います.データ読み込みのための関数が複数あってややこしいですが,ここでは以下の二つに絞ります.ポイントは「データをどんな型として読み込むのか」「データをどこまで読み込むのか(終端をどう判断するか)」です.
  • String Serial.readString()
    シリアルポートから文字列データを読み込み,結果を文字列として返します.データの終端は文字列のデリミタ(\0)がデフォルトですが,readStringUntil()を使うと終端文字を指定できます.
  • int Serial.readBytes(byte[] buffer)
    シリアルポートからバイトデータを読み込み,読み込んだバイト数(int)を返します.データはbufferのサイズいっぱいまで読み込まれますが,readByteUntil()を使うと終端バイトを指定できます.
※Serial.readChar() および Serial.read() は上記二つの1バイト版にあたります.

文字列通信は非効率

入門記事やサンプルコードなどを参考にすると,一番楽な Serial.print/Arduino とSerial.readString/Processing の組を使ってしまいがちです.任意のデータ型をそのまま送れて,また結果をシリアルモニタなどで視認できるので使いやすいことは確かですが,データをいったん文字列の形にして送るので効率はよくありません.たとえば 123 という値を送るには
Arduino側:
Serial.print(123);
Processing側:

String inBuffer = myPort.readString(); int data = int(inBuffer);
などとしますが,このとき {'1','2','3'} = {0x31, 0x32, 0x38}という3バイトが通信されます.一方,Serial.write/ArduinoとSerial.readBytes/Processingの組ならば
Arduino側:

Serial.write(byte(123));

Processing側:

byte[] inBuffer = myPort.readBytes(); 
byte data = inBuffer[0];

のようにしますが,これだと 123 = 0x7B という1バイトの通信ですみます.
long intのように桁の多い整数(時刻など)やfloatなどをやりとりするとこの差はさらに大きくなります.例えば long int は4バイトですが,10進整数に直すと-2147483648〜2147483647ですから文字列では最大で11バイトも食います.

通信速度を57600bpsとすると,57.6bit/msec ≒7byte/msecですから,オーバーヘッドを無視しても1msecでたかだか数バイトしか送れないことになります.前の記事で書いたようにArduinoは1〜2msec くらいの周期でリアルタイム制御ができるのですが,シリアル通信で何msecも消費してしまっては台無しです.リアルタイム性を維持するためには
  • 文字列通信ではなくバイト通信を使う
  • 送信周期を間引く(数ループに1回だけデータを送信する)
などの配慮が必要です.
(続きの記事はこちらです)

Arduinoのリアルタイム性能


Arduinoを使ったリアルタイム制御,とくに母艦PCとのシリアルデータ通信をしながら制御するときの注意点についてまとめておきます.
ここで試しているArduinoはArduino Fioです.ハード概要は以下の通り.

Arduinoでどのくらいの制御周期が出せるか

loop() 関数の中に適当なリアルタイムコードを書いてウェイトなしで全力疾走してみます.以下はよくある制御則(AD入力→1次のフィルタ+線形状態フィードバック→DA出力)を実装した場合です.1ループごとにON/OFFを反転するテスト信号を出してオシロで確認してみます.


上図のカーソル間隔は720us程度.当然ばらつきはありますが,だいたい800us (1.25kHz) くらいの制御周期は出せると考えてよいでしょう.少々の演算なら長めに見積もっても1〜2ms以内で終わるようです.


疑似タイマ割り込み

Arduinoにはタイマ割り込みがありません.したがって,一定周期でループをまわしたい場合は,delay()関数を使ってウェイトの自動調整をする必要があります.たとえば
long int s_time, e_time;
const int dT = 1000;  /* sampling period in [usec])
...
ループの始めで:
s_time = micros();
ループの終わりで:
e_time = micros();
delayMicroseconds(max(0, s_time+dT-e_time)); 
のようにすると約 1ms で回せます.このときの結果が以下の通り.


多少ばらつきはありますが,おおむね指定周期をキープできています.

さて,マイコンとPCの間でリアルタイムでデータ通信(特に無線通信)ができると非常に便利ですが,迂闊に使うととんでもなく時間を食います.これについては次の記事で.

追記:リアルタイムの限界性能
テスト信号のON/OFF以外に何も処理をせず,全開で回したときのループ周期は 460μsec ほどでした.これが限界の性能です.Arduinoには初心者にとって非常に親切なファームウェアが入っている分,オーバーヘッドもそれなりにあるようですね.

6/22/2011

タングステンLove



私が研究しているロボットの中には形状変化による質量移動,要は「おもりを振り回す」ことを動作原理の基本においているものがいくつかあります.たとえば球体ロボットや半球ロボット,三脚歩行ロボット,スネークボードなどがそうです.そこで,おもりが効果的に働き,かつコンパクトなロボットを設計しようと思うと,質量がなるべく一点に集中したおもりが欲しくなってきます.(慣性モーメントと質量の比をなるべく大きくすると言い換えてもよいでしょう).

ロボットの構造には軽い金属の代表であるアルミを使うことが多いです.一方,重い金属としてはまず金が思い浮かびますが,もちろんロボットに使うには高価すぎます.工作でよく使う金属の中では鉄や黄銅(真鍮)などが比較的重い部類に入ります.代表的な材質のおよその密度をまとめてみますと

  • 白金族元素(白金21.45,イリジウム22.42,オスミウム22.57)
  • 金 19.32
  • タングステン 19.3
  • ウラン 18.95
  • 水銀 13.55
  • 鉛 11.35
  • 銅 8.96(黄銅は銅と亜鉛との合金)
  • 亜鉛 7.13
  • 鉄 7.87
  • アルミ 2.70
  • 水 1.0
(単位はg/cm^3)となります.

白金族は金よりも重く,金よりも稀少で高価です.水銀や鉛は安価でそこそこ重く,加工しやすいのでよく使われますが,安全性の面で問題があります.劣化ウランは論外ですね.一番手頃なのはやはり鉄と黄銅です.鉄は硬くて加工が面倒ですが,黄銅は研究室の工具でもサクサク削れて,見た目も美しいのでGoodです.

私がいま熱く魅かれているのがタングステンです.密度は金とほぼ同じ.その名も日本タングステンという会社が製造・販売しています(超硬工具と超硬合金を対決させるTV番組で,工具側として出演していました).合金の場合は純タングステンより密度は落ちますが,それでも16〜17g/cm^3程度,黄銅の倍です.レアメタルなので入手は容易ではありませんが,一度使ってみたいものです.

ちなみにこれまでのロボットについては,半球ロボットSurface Walkerでは釣り具屋で売っていた鉛のおもり,球体ロボットVolvotでは黄銅,三脚歩行ロボットMartian初号機では鉄のおもりを使っていました.

P.S.昔,私の仲間うちではよく「アルミは軽くて重くて軽い」と言われていました.アルミは確かに軽いのだけれども,むやみに部品を増やしていくとすぐに全体が重くなる.これはいかんといって肉抜き(部品に穴を開けたり溝を掘ったり)して軽量化しようとしても,削りカスも軽いためになかなか重量が減らない,というわけです.重量制限のあるロボコンなどの経験から生まれた名言です.

4/15/2011

ArduinoとProcessingのデータ型

  • Arduino IDE では char はsigned, byte はunsigned, int は2バイト
  • Processing では char はunsigned, byte はsigned, int は4バイト
である.これに気をつけないと,ArduinoとProcessing間でデータ通信などをしたときにはまる.なんでこんなややこしいことになっているのやら.

※追記:こういう比較のページもあるにはありますが,細かいことまでは書いていないですね.