SLAMに関する覚書

はじめに

何故こんなものを書いたのか?

 近年,自律移動ロボットにまつわる言葉として
SLAM」という技術をよく耳にするようになった.
しかし(主に付け焼刃で解説"もどき"を披露する一部マスコミによって)
その技術はまるで街中をロボットが自由に動き回れるようになる魔法のようなものとして扱われ,
その本質を正しく理解しようとしている人の障害となっているように思える.
特にこれは,学生としてこれからロボティクスの研究を志す学生にとって有害である.
(逆に,既に技術としてこれを習得なり取り扱っている学生・研究者には無意味な話である)

 そこで私はここにSLAMの"より正しい"理解を目的として覚書を残しておくことにする. とはいえ,私がSLAMをどう理解しているか?という視点になってることは留意していただきたい.

技術の理解に関する個人的方法論

 個人的な方法論として,新しい技術を理解するには,
まずその歴史(何故その技術が必要とされたのか?)と
思想(どのようなアプローチで考えるのか?)
から考えることにしている.

 一般にこの手の工学的な分野では数式を用いて解説をする人も多いが,
その方式は正確で定量的ではあるが直観的な理解には劣り,
本来技術が目指していた方向を見失う可能性もある.
(逆に言えば,技術の水平思考の取っ掛かりとして考えるときには有用な方法である.)
また,工学分野における数式とは思想や現象のモデル化であり,
最初にその技術を編み出した人はまず間違いなく
定性的なアルゴリズムを数式モデル化するプロセスを踏んでいるはずである.

そもそもSLAMとは?

自己位置推定と環境地図

 閑話休題,SLAMの話に戻ろう.

 SLAMとは"Simultaneous Localization and Mapping",
「自己位置と環境推定を同時に行う」 ということ,言い換えると
「自己位置推定と環境地図作成の間の矛盾を解決する」
ということである.

 人間が未知の環境(たとえば初めて訪れる町)で目的地にたどり着くには
“自分の位置と目的地を含む地図”が事前の知識として必要になる.
(目的地までどのように向かうか?
例えば
“大通り優先で迷いにくいルート”
“裏道使ってでもとにかく所要時間を短くするルート”
“地形を無視して進路上の全てをぶち壊しながら進む最短距離ルート”
等と言ったルート選択の問題は別に考える.)

 また,人間が未知の環境に放り込まれた時も,
手元に「自分の位置を含む(但しどこかはわからない)地図」があれば,
周囲の地形と比較することで自分の位置を推定することが可能である.

 このように,地図は自律行動するロボットにとって最も重要と呼べるものであり,
その「正確さ(正確さの定義は別に考える必要があるが)」
ロボットの最終的なパフォーマンスに大きく影響する.

環境地図の構築

 この「地図(環境地図)」であるが,これはどのようにして生成するか?
例えば,3DCADを使って精密に環境を設計し,その設計どおりに環境を構築したならば
その3DCADデータをそのまま地図として使えるだろう.
また,環境の全てにメジャーと分度器を当て,それぞれの長さと角度を図面に書き写せたなら,
それもまた有用な環境地図として取り扱うことができるだろう.

 しかし,現実には環境は設計どおりではない上に,
(自然環境などにはそもそも設計図そのものがないし,
建物のであっても後付で棚や扉を設置するのは良くあることである.)
全てを人力で計測して環境地図を描くのは現実的な方法ではない.

 そこで考えられるのは,ロボットに搭載したセンサを用いて環境地図を得る方法である.
現在のロボットでは,センサとしてLRF(レーザー測距計)やステレオカメラを用いる方法が一般的だ.

環境図構築の問題点

視野の問題

 しかしこれで万事解決と言うわけにはいかない.
これらのセンサは環境の全てを見渡せるわけではないので,
当然得られる環境地図もきわめて限定的である.
また,精度も場所によって(特にセンサからの距離に依存して)低下する.
(例えばステレオカメラによる位置計測は,原理的にカメラからの距離の二乗に反比例して精度が下がる.)
環境の全てをセンサの有効範囲に収めようとすれば,
航空写真や衛星写真のように距離をとる必要がある.
しかしこれは先に述べたようにセンサの精度を低下させる事がほとんどであり,
また屋内のようにそもそも環境全てを見渡せる場所が存在しないこともある.

局所地図貼り合わせ

 ではどうするか?一番簡単な方法は「小さい地図を貼り合わせる」という方法である.
やっていることは伊能忠敬が日本地図を作ったときと同様,
センサの有効範囲で取れる小さい地図(一般にこれを局所地図あるいはローカルマップと呼ぶ)を
複数取得した上で,それぞれの地図をセンサを取得した位置ごとに貼り合わせ,
環境地図を作成する手法である.

 さあ今度こそ万事解決・・・ではない.
なぜなら,複数の地図を貼り合わせる際には
「その地図はどの位置(≒センサの位置)から見た地図なのか?」と言う情報が必要なためである.
位置がずれれば,当然貼り合わせた地図はゆがんだものとなり,
各々の局所地図が正確であったとしても,環境地図は正確にはならないのである.

 これはかなり大きな矛盾を持った問題である.
何故なら,センサの位置を正確に知ることができているのなら,
それは環境地図作成の目的である「自己位置の推定が達成されている」
と言うことと全く同義
であり,そもそも環境地図が必要ないのである.

SLAMとは

 正確な自己位置のためには正確な環境地図が必要
正確な環境地図作成のためには,正確な自己位置が必要・・・
この矛盾に対処するための技術が“SLAM”なのである.

 したがって「自律移動ロボットにSLAM技術を搭載!」というのは少々正確ではない.
自動運転車などの自律移動ロボットは,
事前に正確な環境地図が与えられていることが大半であり,
正確な環境地図があれば後はそれを元に自己位置を推定し,
目的地を設定し,ルートを決定し走行するだけである.
そこに上記の矛盾を解決する技術など必要ない.

SLAMの実装

理解の出発点:ICPアルゴリズム

 さて,SLAM技術が何のために必要とされているのか?と言う点については上記の通りである.
では,具体的にどのようなアルゴリズムによってこれを実現しているのだろうか?

 最も直観的な考えでこの問題について考えるとするならば,その答えは
「局所地図同士をオーバーラップさせ,そのすりあわせによって地図と位置を更新する」
方法であろう.

 一例として2つの局所地図(A,B)を貼り合わせる事を考える.
2つの局所地図はある程度(6割~7割)が同じ地形を捉えていると考えた場合,
その部分は局所地図ABの間で一致しているはずである.

 そこで,片方の局所地図Aを基準として,
もう片方の地図Bをいろいろと捻ったりずらしたりして
もっともぴったり合うような合わせ方を模索する.

 このとき局所地図同士の「もっともピッタリな合わせ方」を見つけ出したら
両者の地図の和を取ることで局所地図Aを拡張することができる.
(これは「未完成の環境地図」であると考えることができる)
この際,局所地図Bの中心(つまりセンサの位置)が,
未完成の局所地図の何処にあるか?を考えれば,
そこが新しい自己位置であると考えることができる.

 さらにロボットが移動し局所地図Cを取得したならば,
(当然,局所地図CはBとある程度同じ地形を捉えている)
「未完成の環境地図」と局所地図Cの間で「もっともピッタリな合わせ方」を模索し
未完成の環境地図を拡張することができる.

 このように,地図同士を繰り返し重ね合わせて,最も収まりの良いあわせ方を
模索するアルゴリズムを,ICP(Iterrative Closest Point)という.

 これはSLAM実装における最も直観的な考え方だろう.

ICPの欠点

 ICPアルゴリズムは最も直観的にわかりやすいアルゴリズムだが,
当然コレだけで話が終われば苦労はない.ICPの問題点を挙げていこう

時間がたつにつれ計算時間が指数関数的に増大する

 ICPの基礎は,局所地図と未完成の環境地図を重ね合わせて,
最も収まりの良い点を探すことである.当然,未完成の環境地図は
局所地図を重ね合わせるたびに広がっていく.
仮に局所地図を未完成の環境地図の全領域に重ね合わせて調べようとすれば,
未完成の環境地図が広がれば広がるほど計算時間が発散してしまう.
また,格子状の地形のように,別の場所に同じような地形が存在している場合
局所地図のあわせ込みが間違った場所になってしまうことにもなりかねない.

 これに関しては,マップの重ねあわせを
「前回局所地図を環境地図に重ね合わせた際に推定した位置」
周辺に限定することである程度の解決が可能である.

精度が下がる

 局所地図を環境地図に統合する際,ほとんどの地形は既に環境地図に
書き込まれている場所なため書き足す必要はない.
またそれら「書き足す必要のない」領域は局所地図の中心付近である.
したがって新しく「書き足される」領域は地図の端っことなるが,
先に述べたように,センサから遠い領域は一般的に精度が悪い.
つまり,環境地図に書き足される領域は常に精度の悪い領域であり,
その領域を環境地図として組み込んだ地図を元に,次の局所地図を
組み込もうとすれば,その位置の合わせこみの精度はどうしても下がってしまう.

ループ閉じこみ問題

 ICPのアルゴリズムは,一般に環境地図と局所地図は常に正確であることを前提としている.
定かではないのはセンサの位置≒自己位置であり,環境地図と局所地図を重ね合わせることで
自己位置を推定しようと試みる.

 しかし,先に挙げた理由により,(未完成の)環境地図は広がれば広がるほど誤差を多く含む.
当然,誤差を含む環境地図を元に局所地図を取り込んで拡張した環境地図はより多くの誤差を含む
したがって,地図は徐々にゆがんでいくこととなる.

 この問題が一番良くわかるのがループ閉じこみ問題である. 下図では,建物の周りを一周してその環境地図(とロボットの軌跡)を推定している. 当然,一周しているのだからスタートとゴールは同じ点になり,軌跡は長方形を描くはずだが, 局所地図を重ね合わせる際に徐々にずれていった結果,その軌跡はループを描くことなく
そのスタートとゴールは「違う地点」と認識されてしまっている.

次あたりで「確率ロボティクス」の概念を組み込みながら
SLAMの定性的な理解と基礎的なモデリングを考えてみる.

それでは,また

MPU9250で地磁気センサを使う方法(SPIで)

はじめに

数年前から,やれIoTだなんだという言葉がバズワードっぽく流行っている.
そのおかげか知らないが,安価で小型なセンサが数多く出回るようになり,
ロボット製作を目指すものとしてはうれしい限りである.
そんな中でも一番需要の高いセンサと言えばIMU(完成計測装置)であろう.

なんせ,二足歩行させるにも,ドローンを飛ばすにも姿勢の安定は最重要課題である.
そのため,姿勢を計測できるセンサの精度は非常に重要である.
当然,私が以前作っていた倒立二輪型のロボットでも,この手のセンサは最重要部品の一つと言えるだろう.

数ヶ月前,Aliexpressで9軸のIMUを見つけ,思わず購入してしまった. ja.aliexpress.com

このモジュールに載っているのはInvenSense社(現在はTDKが買収)のMPU9250というチップ.
前に使っていたMPU6050の後継機種ともいえるもので,最大の特徴はMPU6050が6軸(ジャイロ・加速度)だったのに対し, MPU9250は9軸(6軸+地磁気)となったこと,そして通信方式がSPIとI2Cの両対応になったことだろう. 1個あたり400円しない額で,9軸のデータが取れるのは非常に便利である.

また倒立振子やドローンのような,センサに応答速度が求められる分野に関して,SPIはかなり便利である.
(一般にI2Cは100k~400kbps前後なのに対して,SPIは1~10Mbps程度の通信速度が出せる)

MPU9250をSPIで使ってみよう(加速度・ジャイロ編)

MPU9250の前身のMPU6050は,Arduino向けのセンサモジュールとしてそこそこの数が出荷されていた. そのため,Arduino上のフォーラムでは,かなりのユーザーがI2Cを利用したMPU6050のライブラリを公開している.
MPU6050の後継であるMPU9250も,同じ会社の同じ種類の製品のため,内部構造は酷似・・・というかほとんど同じである.
そのせいなのか,MPU9250に関するライブラリもI2Cを使用したものが多く,SPIを使ったものはあまり多くはないようだ.

とはいえ,SPIは基本的にI2Cと同じ要領で扱うことができるように設計されている.
レジスタのアドレスや書き込む/読み込むべきデータはまったく同じのようだ.
したがって,SPIを使用する場合は,レジスタへの書き込み方法と読み込み方法さえ書き換えればよい.
I2Cでは,

レジスタ書き込み

I2C

  • 書き込みモードでスレーブアドレス送信
  • レジスタアドレス送信
  • 書き込むデータ送信

SPI

  • レジスタアドレス送信
  • 書き込むデータを送信

レジスタ読み込み

I2C

  • 書き込みモードでスレーブアドレス送信
  • レジスタアドレス送信
  • 読み込みモードでスレーブアドレス送信
  • データ読み込み

SPI

  • レジスタアドレスを受信モード(最上位ビットを1に)送信
  • データを読み込み(空データ送信)

という形になる.むしろシンプルになってわかりやすいが 読み込み時にレジスタアドレスの最上位ビットを1にすることを忘れないようにしなければならない.

MPU9250をSPIで使ってみよう(地磁気編)

次は地磁気なのだが,これはかなり厄介である.
なぜかというと,MPU9250は内部的に
6軸センサ+3軸地磁気センサという構成になっているからだ.
というか,地磁気センサは旭化成のAK8963がそのまま入っているようである.
要するに,I2Cバスでつながっている2つのデバイス
無理やり1つのチップに収めたと言うことのようだ.
その証拠に,I2Cで通信する場合6軸センサと地磁気センサでは,
スレーブアドレスもレジスタアドレスもまったく異なっており,
地磁気センサのそれはAK8963とまったく同じようだ.

では,これをSPIで読むにはどうすればよいのだろう?
SPIも基本的にはバス接続が可能
(ただしチップセレクトだけはデバイスの分だけ用意する必要がある),
であればチップのどこかに地磁気センサ用のチップセレクトがあるのか?
しかし,上のデバイスには該当するようなピンはない.

6軸センサと地磁気センサはお互いにI2Cでつながっているが, SPIは6軸センサにしかつながっていない.
(ただし,両者が同じチップ内に存在するので,
外から見ると「どちらのセンサにもつながっている」ようにしか見えない)

こうなれば答えは一つ,
SPIを通じて6軸センサを操り,6軸センサのI2C機能を利用して地磁気センサから情報を得る
であろう.

実は,MPU9250のレジスタマップには

  • I2C_SLVx_ADDR
  • I2C_SLVx_REG
  • I2C_SLVx_CTRL
  • I2C_SLVx_DO

という4つのレジスタがある(ただしx=0~4である) これは,6軸センサをI2Cのマスタとして,

  • スレーブデバイスI2C_SLVx_ADDR
  • スレーブのレジスタアドレス:I2C_SLVx_REG
  • レジスタに書き込むデータ:I2C_SLVx_DO
  • 送信許可:I2C_SLVx_CTRL

とするI2C通信を行うためのレジスタである.
SPIを使ってI2C通信するというなんか奇妙なシステムになっているが,この際仕方ないだろう.
また,この機能(SPI経由でI2Cを間接的に使用する機能)をコントロールするためのレジスタとして USER_CTRLI2C_MST_CTRLの2つのレジスタの設定が必要になるようだ.

また,この時スレーブデバイスのアドレスを指定するレジスタI2C_SLVx_ADDRの最上位ビットを1にすると 読み込みモードでの通信になる.このあたりはI2Cのアドレス指定とは逆になるので注意. (I2Cではスレーブアドレスを1ビット左シフトして,最下位ビットを1にする.) さらに,読み込み/書き込みバイト数はI2C_SLVx_CTRLの下位4ビットで指定できる. (もっとも,書き込みの場合I2C_SLVx_DOが8bit幅なので最大1バイトしかできないが) この際,受信したデータはEXT_SENS_DATA_00

AK8963で地磁気を読み込むには?

さて,MPU9250のSPI→I2Cブリッジ通信機能を使えば,
内臓地磁気センサであるAK8963にアクセスすることが可能であることがわかった.
では,実際にはどのレジスタに何を書き込めばよいのか?
AK8963のデータシートを見るとこのあたりは結構楽そうである.
手順は以下の通りである.

  1. CNTL2に0x01を書き込んでデバイスをリセットする(たぶん不要な手順だが安定のため.)
  2. CNTL1に動作モードを書き込んでセンサを起こす
  3. 計測値がHXL以下6バイト分に格納されるので読み込む
  4. 手順2において,動作モードが連続測定モードの場合,ST2レジスタを読み捨てる
    (こうしないと次の計測値が入ってこない)

これで良い様だ.レジスタマップ上ではHXL以下6バイト分の計測データと,
ST2レジスタが連続しているため,手順3において7バイト読めばよいようだ.
(最後の1バイトはST2なので読み捨て)

まとめ

そんなわけで,MPU9250の地磁気センサをSPIを使って読む場合の手順は以下の通り.
(連続計測モード100Hzを想定)

初期化関数

  • USER_CTRLI2C_MST_CTRLの2つのレジスタを設定して,
    SPI経由のI2C通信を有効にする
    USER_CTRL:0x30(機能を有効化)
    USER_CTRL:0x0D(I2Cクロックを400kHzに設定)
  • I2C_SLVx以下を設定して地磁気センサをリセット
    I2C_SLVx_ADDR:0x0C(AK8963のアドレス)
    I2C_SLVx_REG:0x0B(CNTL2) I2C_SLVx_DO:0x01(リセット有効)
    I2C_SLVx_CTRL:0x81(1バイトのデータ送信)
  • I2C_SLVx以下を設定して地磁気センサを連続送信モードに
    I2C_SLVx_ADDR:0x0C(AK8963のアドレス)
    I2C_SLVx_REG:0x0A(CNTL1) I2C_SLVx_DO:0x16(連続計測モード100Hz)
    I2C_SLVx_CTRL:0x81(1バイトのデータ送信)

計測関数

  • I2C_SLVx以下を設定して地磁気センサをリセット
    I2C_SLVx_ADDR:0x8C(AK8963のアドレス.受信なので最上位ビットが1になっている)
    I2C_SLVx_REG:0x0B(HXL) I2C_SLVx_CTRL:0x87(7バイトのデータ受信)
  • EXT_SENS_DATA_00から7バイト分受信
    (先頭6バイトが地磁気データ)

※受信したデータは下位8ビットが先行しているので,16ビットデータに直す際は注意が必要
※当然だが地磁気は100Hzの計測周期なので,
毎回地磁気を取得しているとせっかくSPIが高速なのにその利点を生かせない

それでは,また.

光に関するエトセトラ

はじめに

仕事でLED照明を扱うことになったので,基礎の基礎知識をおさらいしたい.
とりあえずは「明るさは工学的にどう定義されるのか?」である.

明るさの定義

あるLED電球から光が照射され,その光が地面に当たって照らしている. そんな状況を考え,その「明るさ」を何かしらの数字であらわしたいと考える.
(でないと,仕様書に「~~以上の明るさのライトを選定してください」と言えないしね.)
まず最初に考えられるのは,ライトが発する総光線数であろう.
これが低いライトではそもそも周囲を照らしようがない.

ただ,この数値だけで照明の性質を全て表現できるかと言えばさにあらず.
同じライトを使っても,全方位に照らすのとミラー等を使って収束させ一方向に照らすのとでは明るさは異なるだろう.
となると,もう一つの数値として任意の空間を貫く光線の数というのも重要な物理量だ.
ちょうどこの辺の考え方は電気力線の考え方と同じではないかと思う.

じゃあこの二つで十分なのか?と言われるとさらにそうではないはず.
実際にライトで照らされたモノがどの程度の明るさを持っているのかを考える必要がある.
当然,同じ光源・同じ収束率で照射される照明であっても,
その当たる面が雪原のような物質なのか,あるいは黒体のような物質なのかで明るさは変わる.
よって,光が当たった面の明るさというのも重要な評価ファクターだろう.

さて,ここまで考えると明るさというものを一種の電気力線として考えればいいように思えてくる.
対応は以下の通りだ.

電荷ライトが発する総光線数
電気力線の本数:任意の空間を貫く光線の数
静電気力:光が当たった面の明るさ(どのような物質に照射されるかに依存するため)
である.さらに,電気力線の本数を単位面積で割ったもの(つまり電気力線の密度≒電束密度) 電束密度光線密度

といった考えができるだろう.

光の単位系では,それぞれを ライトが発する総光線数:ルーメン
任意の空間を貫く光線の数:カンデラ 単位面積あたりの光線密度:ルクス
と言うらしい.

設計にはどう使うの?

じゃあ設計時に気にしなきゃいけない値は?というと,おそらくカンデラであろう.
まずルクスは当てにならない.単位面積当たりの光線密度なのだから,
同じ照明でも近くを照らせば高く,遠くを照らせば低く値が出てしまう.
測定環境を限定した上での数値基準を設けるのであれば良いのだが.

次に気にしなくて良いのはルーメンであろう.
スポットライトのように,特定の方向に光を収束させるものであれば,
たとえルーメンが小さい値であっても,照らされた面は明るくなる.
ただ,同じ構造なら当然ルーメンが大きいほうが明るくなるのは必然である.
特定の方向(つまり自分が使いたい方向)にどの程度の強さの光が出ているのか?
という感覚で考えると,カンデラが一番わかりやすいだろう.

よって,設計の手順としては

  • まず,光を照射させたい範囲(むしろ方向)を考える
  • その方向において,特定の距離でどの程度の明るさが欲しいのかを考える
  • その明るさを実現できるカンデラを考える
  • その方向にそのカンデラの光を与える光源と構造を考える

とするのが良いはずだ.あるいは,この逆に考えて

  • まず照明のルーメンを決定する
  • 次に,照射範囲を決定し,カンデラを計算する
  • 最後に,想定する状況(光源との距離)下におけるルクスを計算し,どの程度明るいか見積もる

でも良いかもしれない.

計算に関しては諸々面倒なので以下のサイトが使いやすそうである.

http://tomari.org/main/java/hikari.html

それでは,また.

SPI通信覚え書き

はじめに

EEPROMやセンサとマイコンの通信に使われるインターフェースとして,
I2Cに並んでよく使われるのがSPI.

線の数は増える(I2Cの約2倍)だし,
バイスをつなげばつなぐほどホストのCPUのピンを圧迫するが,
基本的にI2Cよりもずっと高速でかつ仕組みが単純で,
アドレスの衝突を気にしなくても良いため,未だに人気のインターフェースと言えよう.

さて,SPIの通信には4種類のモードがあるのは良く知られている話である.
各々4種類の通信モードには,

  • クロックが立ち上がり先行か,立下り先行か
  • ラッチが立ち上がり時か,立下り時か

の2つのパラメータがあるため,2×2で4種類であると言うことである.
で,このモード,個人的に非常に覚えにくいのでまとめの図と簡単な解説を
ここに覚え書きとして書いておくことにする.

クロック(CPOL)

クロックの立ち上がり先行か立ち下がり先行か,と書くと微妙にわかりにくいが,
要は
「バスオフ時(SPIで何処とも通信していない時)に,クロックラインがHなのかLなのか」
と言うことである.
クロックラインが普段L(CPOL=L)であれば,当然通信を開始するとき(=クロックを生成するとき)は まず最初にクロックラインがL→Hになる.
逆にクロックラインが普段Hであれば,クロックラインはまずH→Lになると言うことである.

また,クロックの論理にもこれは絡んでくる.

普段Lのクロックラインがクロックを生成するのであれば,
それは必ずL→H→L,の順でクロックが生成される.
図を描いてみればわかるが,これは凸型のパルスになるので,
正論理のパルスと考えることができる.
その逆にクロックラインが普段Hであるならば,
クロックは必ずH→L→Hの凹型の負論理のパルスとなる.
このときパルスの前半(正論理ならL→H,負論理ならH→L)をパルスの前縁
パルスの後半(正論理ならH→L,負論理ならL→H)をパルスの後縁
と言い表す.

ラッチ(CPHA)

SPIを実際にCPUに実装する(あるいは,FPGAにSPIインターフェースを実装する)際,
良く使われるのがレジスタと書き込み読み込みを行うヘッダを用意し,
クロックが来た際に1ビットのデータを読み込む/書き込む→ヘッダを一つずらす
と言う実装方法であろう.
このとき,データの読み書き作業をラッチ,ヘッダをずらす動作をシフトと言う

クロックはH->L,L->Hで1セットなのだから,
データの読み書きとヘッダをずらす作業をそれぞれのタイミングで行う形になる.
それぞれ別にすることで,ヘッダをずらしている最中にデータを読みにくい,
など通信が不安定になる要素を排除できる.
つまるところ,ラッチをL→Hで行うのか,H→Lで行うのか(シフトはその逆で行う)
で2通りの通信方法があるわけだが,これをパルスの前縁,後縁のどちらで行うのか?
という書き方で説明しているものが多い.

上のパルスの項で説明したが,パルスの前縁・後縁と言っても,
パルスそのものが正論理なのか負論理なのかで意味するものが逆になってしまう.
正直,図を書くときにわかりにくいので,
ラッチ(読み書きを)L→Hで行うのか?H→Lで行うのか?
で覚えるほうが楽であろう.

まとめ

そんなわけでまとめである.
このエントリはほとんどこの表を書くためだけに存在する.
上の文章は,ただ単に表を張るだけではかっこ悪いと言うだけである.

普段バスがL (CPOL=L) 普段バスがH (CPOL=H)
立上りで読み書き (CPHA=L->H) モード0 モード3
立下りで読み書き (CPHA=H->L) モード1 モード2

それでは,また.

I2C通信まとめ

はじめに

マイコン工作でよく使う通信方式と言えば,概ね

  • UART
  • I2C
  • SPI

を挙げることに異を唱える人はいないだろう.
(カメラとか作る人はLVDSとかその辺も入ってくるけど)
このうち,UARTはどちらかと言うと基板内ではなく基板間
(というか大抵FT232RL通してパソコンと接続)
で使うことが多いので,基板内通信で使うものと言えばSPIとI2Cが主流である.

このうち,SPIは単純なシステム故にわかりやすい.
送受信がそれぞれ別の信号線を用いているうえに,アドレスでデバイスを指定する必要が無い.
バイスが増えると増えただけnCSを必要とする以外は使いやすいだろう.

問題はI2Cである.
I2Cもマイコン用の通信規格である以上,そこまで複雑なことはさせていない.
しかし,SPIと異なり色々と「初心者がハマる」ポイントがいくつかある.
しかも悪いことに,標準的に使われる規格である以上,
ArduinoWireライブラリやmbedのライブラリを駆使しつつサンプルコードをコピペして動かすだけの
資料がそろってしまっているのである.
単にサクッと動かしたいだけならそれはありがたいのだが,
ライブラリのないデバイスをデータシート頼りで動かそうとするとさすがに中身を知らないと
バグの取りようがない.

そこで,一念発起して一通り今まで理解したことを書いておくことにする.

I2Cでの通信

基本的な通信の流れとして

  1. 【マスタ→スレーブ】スタートコンディションを送る(というか示す)
  2. 【マスタ→スレーブ】アドレス(7ビット+リードライトを示すビットで8ビット)を送信する
  3. 【スレーブ→マスタ】該当するアドレスを持つデバイスはマスタに向けてACKを返す
  4. 【双方向】送受信する
    • 送信の場合
      1. 【マスタ→スレーブ】データを1バイト送信する
      2. 【スレーブ→マスタ】ACKを返す
      3. 上二つを送りたいデータ数分だけ繰り返す
    • 受信の場合
      1. 【スレーブ→マスタ】マスタのクロックに合わせてデータを送信する
      2. 【マスタ→スレーブ】ACKを返す
      3. 上二つを受け取りたいデータ数-1だけ繰り返す
      4. 【スレーブ→マスタ】最後のデータを受け取る
      5. 【マスタ→スレーブ】NACKを返す
  5. 【マスタ→スレーブ】ストップコンディションを送る(というか示す)

このようになる.

送信

概ね前の章で書いたことがI2Cの送受信のやり方だが,
送信だけなら難しいことはほぼ何も無い.

  • スタートコンディション
  • アドレス送信
  • データ送信
  • ストップコンディション

でおしまいである.あとはアドレス送信とデータ送信の間にスレーブからACKが返ってくるので,
それを待つことさえ忘れなければ良い.

受信

問題は受信である.ここで変に勘付いて「どうせ送信と逆でしょ」と

  • スタートコンディション
  • アドレス送信
  • データ受信
  • ストップコンディション

という流れを考え,
「データ受信したらマスタからスレーブにACKを送っておけばOK」
と思い込むとハマる.
結論から言えばその考えは80%は間違ってなかっただろう.
「最後の1バイトを受信したら,ストップコンディションの前にACKではなくNACKを返す必要がある」
という点には十分な注意が必要である.

ACKと言う信号は,一般には通信応答の意味を示す.これは
ACKを返す=データを正しく受信した
ACKを返せない(NACKを返す)=データが正しく受信できていない
という意味になる.マスタからスレーブに送信する場合, ACKの意味は上記で間違いない.

しかしI2Cの場合,マスタが受信する際に送るACKは
「データをさらに要求するかどうか?」
という意味に変化し,正しく受信できたか?という意味にはならない.
ここを勘違いしたままでいると,
(そしてArduinoライブラリなど,このあたりの処理が隠されたライブラリを使っていると),
最初の1回は上手くいくのだがそれ以降通信がタイムアウトしてしまう.

受信におけるACKとは,
ACKを返す=さらにデータを要求する
ACKを返せない(NACKを送る)=データの要求を終了する
と言う意味である,ということを念頭に置かないと処理がおかしくなってしまうのである.

レジスタを使った通信

さて,一般的なセンサデバイスと通信する場合,計測値の受信や設定値の送信は
バイス内のレジスタに対する読み書きという形で表現されることが多い.
これをI2Cの流れに沿って考えると,

**レジスタに書き込む

  1. 【マスタ→スレーブ】スタートコンディションを送る(というか示す)
  2. 【マスタ→スレーブ】アドレスを書き込みモードで送信する
  3. 【スレーブ→マスタ】該当するアドレスを持つデバイスはマスタに向けてACKを返す
  4. 【マスタ→スレーブ】書き込み対象のレジスタアドレスを送る
  5. 【マスタ→スレーブ】レジスタに書き込むデータを送る
  6. 【マスタ→スレーブ】ストップコンディションを送る(というか示す)

これで書き込みは実現できる.もちろん,レジスタアドレスやデータを送る際には,
スレーブからACKが返ってくる.

次に,レジスタから読み込みたい場合はどうすればよいだろう?
レジスタアドレスの指定はマスタ→スレーブなのだから書き込みモードだが,
読み込むのはスレーブ→マスタなのだから読み込みモードのはず・・・ ではデバイスにはどちらのモードで接続をかけるのが正解なのだろうか?

まず,余計なことを考えずに「送信と受信」で考えると以下のようになる.

  1. 【マスタ→スレーブ】スタートコンディションを送る(というか示す)
  2. 【マスタ→スレーブ】アドレスを書き込みモードで送信する
  3. 【マスタ→スレーブ】読み込み対象のレジスタアドレスを送る
  4. 【マスタ→スレーブ】ストップコンディションを送る(というか示す)
  5. 【マスタ→スレーブ】改めてスタートコンディションを送る(というか示す)
  6. 【マスタ→スレーブ】アドレスを読み込みモードで送信する
  7. 【スレーブ→マスタ】データを送信する
  8. 【マスタ→スレーブ】最後だけNACKを返す
  9. 【マスタ→スレーブ】ストップコンディションを送る(というか示す)

「送信するものは送信し,受信するものは受信する」で処理を分けた.
モノにもよるがこれでうまくいってくれるデバイスは多い.
実際には,このうち4.を省略することが可能であるし,
一般にはそのようにすることが多いようだ.その場合このようになる.

  1. 【マスタ→スレーブ】スタートコンディションを送る(というか示す)
  2. 【マスタ→スレーブ】アドレスを書き込みモードで送信する
  3. 【マスタ→スレーブ】読み込み対象のレジスタアドレスを送る
  4. 【マスタ→スレーブ】改めてスタートコンディションを送る(というか示す)
  5. 【マスタ→スレーブ】アドレスを読み込みモードで送信する
  6. 【スレーブ→マスタ】データを送信する
  7. 【マスタ→スレーブ】最後だけNACKを返す
  8. 【マスタ→スレーブ】ストップコンディションを送る(というか示す)

この場合,4.のスタートコンディションは別名「リスタートコンディション」と呼ばれる.
しかし,実態は見ての通りスタートコンディションを送りなおしているだけであって,
リスタートコンディションという特別なコンディションは存在しない

まとめ

そんなわけで,今までハマったポイントをまとめると以下のようになる.

  • 送信は単純.各データを送った際に1バイトごとにスレーブからACKが来る
  • 受信の際はACKの意味が「さらなるデータを要求するかどうか」に変化していることに注意
  • そのため,受信を最後はACKではなくNACKを送ってデータをもらい終わったことを示さなければならない
  • レジスタし指定して受信する際は「スタート,スタート,ストップ」コンディションを送る
  • 2回目のスタートコンディションをリスタートコンディションと言うが,特別なコンディションではない

これを覚えておけば,多少なりともI2Cの挙動が理解しやすくなるし,
また上手く通信ができない場合も原因究明に役立つことだろう.

それでは,また.

Ubuntu & ROS環境覚書

今使っているubuntu+ROSの環境を一応書いておきます.

 

OS:ubuntu 14.04+LTS

ROS:indigo

 

インストールすべきパッケージとインストール方法:

 

ROSのインストール:

ja/indigo/Installation/Ubuntu - ROS Wiki

 

catkinのインストール:

cyberworks.cocolog-nifty.com

 

PTAMのインストール:

さくっとPTAMをROSで試す(1)・・・サンプルデータでPTAMを試す - sabo laboratory

 

SPTAMのインストール:

GitHub - lrse/sptam: S-PTAM: Stereo Parallel Tracking and Mapping

 

UVCカメラを使えるようにするためのパッケージのインストール:

試行錯誤な日々: ROSでカメラ画像を表示

(ただしこれはROSのバージョンが違うので,groovyをindigoに直すこと)

 

ステレオカメラを使えるようにするためのパッケージのインストール:

bril-tech.blogspot.jp

(USBカメラを2個使う場合のもの)

 

Playstation4のステレオカメラをROSで使えるようにするための方法:

orientalrobotics.blogspot.jp

 

とりあえずはこんなところ.

それでは,また.

 

DFUに関して

備忘録的な内容なのですごく基礎的なことを書きます.

最近,仕事でDFUを触る必要性が出てきたので,とりあえずお勉強.
当たり前ですが,組み込みシステムは一度製品を出荷してしまうと
中々ファームウェアのアップデートはできません.

無論,それでもやらなきゃならないことはあるので,
そのために弄する手段の一つがDFU(Device Firmware Update)

通常,マイコンJTAGやSWDといった専用のインターフェースを用いて
書き込み・デバッグをします.
しかし,こんなものは製品にとっては無用の長物であるばかりか
CPUの中身を覗き放題なので出来る限り潰したいわけです.
しかし,先に書いたようにファームウェアのアップデートだってやりたい・・・

そのため,UARTやCAN・I2Cなど,そもそも別の用途に使っている
(つまり引き出している)インターフェースから書き込めるようにしようと
考えるのは自然の摂理と言えるでしょう.

マイコンにプログラムを書き込むという行為は,
極論してしまえば「メモリのプログラム領域にコンパイルしたデータを書き込む」
行為に他ならないわけで.

ぶっちゃけて言えば,
「母艦から好きなインターフェースでデータを受け取り,プログラム領域に書き込むプログラム」
マイコンのどこかにあれば,それを起動して話は解決というわけです.

多くのマイコンでは,「マイコンどこか」をDFU機能のための領域として確保しています.
ここに先に書いたようなプログラムを開発・書き込んでおけば問題ないというわけですね.

さて,このDFUという機能,一番わかり易いのがArduinoだと考えられます.
通常,Arduinoに使われるAVRマイコンにプログラムを書き込むには,
ICP(In Circuit Programmer)端子を利用して書き込む必要があります.
まぁ,その正体はただのSPIではあるのですが.

しかし,実際のArduinoはUSB,回路でUARTに変換したものを使って書き込んでいますね.
要は,ArduinoのAVRマイコン内部には,UARTからのデータを使ってマイコン
プログラムを書き込む機能(プログラム)が備わっているというわけです.

Arduinoにおいては,これをブートローダーと読んでいますが,
機能的にはDFUに近いものとして考えることが出来るでしょう.