Servo ライブラリ
Arduino の Servo ライブラリ を用いると、設定する角度を指示し、サーボ制御用の PWM 信号を出力することができる。
このライブラリを使用する際、制御を行う Servo オブジェクトのインスタンスで attach 関数を呼び出すことにより、信号を出力するピンを指定する。
このとき、角度「0度」(min) および「180度」(max) に対応するパルス幅を設定することもできる。
#include <Servo.h>
Servo s1, s2;
void setup() {
s1.attach(5); // デフォルトのパルス幅の範囲を用いる
s2.attach(6, 600, 2000); // 「0度」を 600 us、「180度」を 2000 us にする
}
void loop() {
}
いいこと思いついた?
attach 関数で指定できる「0度」および「180度」の角度の範囲について、ドキュメントには特に記載が見当たらない。
そのため、一見「常識的」な範囲で任意の角度を設定できそうに見える。
そこで、有効なパルス幅の範囲のうち一部を指定し、その範囲内を高い解像度で動いてもらおうとした。
#include <Servo.h>
Servo servo;
void setup() {
servo.attach(5, 600, 800); // 600 us ~ 800 us の範囲で動いてもらいたい
servo.write(90); // 真ん中の 700 us を出力したい
}
void loop() {
}
これを実行すると、仕様通り約 20 ms 間隔でパルスが出力された。
しかし、出力されたパルスの幅は 1725 μs となっており、期待した 700 μs からは約 2.5 倍という非常に大幅な誤差が生じてしまった。
どうしてこうなってしまうのだろうか?
ライブラリの実装を見てみる
ドキュメント の「Go to repository」や「Go to repository」から、ライブラリのソースコードが格納されたリポジトリを開くことができる。
このライブラリのライセンスは LGPL である。
見すぎには注意しよう。
attach 関数で min と max を指定すると、それぞれ MIN_PULSE_WIDTH および MAX_PULSE_WIDTH との差が記録される。
MIN_PULSE_WIDTH の値は 544、MAX_PULSE_WIDTH の値は 2400 である。
そして、記録先はなんと int8_t 型の変数なのである。
int8_t は8ビット符号付き整数であり、-128 ~ 127 の値しか格納できない。
逆算すると、min は 33 ~ 1059、max は 1889 ~ 2915 であれば記録する値がこの範囲に収まるが、min や max がこの範囲の外だと記録する値がこの範囲に収まらず、意図しない値が記録されてしまうことがわかる。
実行結果の考察
先ほどの例
servo.attach(5, 600, 800); // 600 us ~ 800 us の範囲で動いてもらいたい
では、min の値 600 は問題ないが、max の値 800 は正しく記録できる範囲から外れてしまっている。
このとき、400 (0x190) を記録しようとし、単純にはみ出した上位ビットを捨てると -112 (0x90) が記録される。
これは、max として 2848 を指定したとき記録される値である。
min に設定した値 600 と、max に設定したとみなされる値 2848 の中間は 1724 であり、観測されたパルスの幅である 1725 μs に近い値となる。
どうすればよかったのか
map() 関数を用いることで、ある範囲の値を別の範囲に変換できる。
この関数を用いて 0 ~ 180 の数値をパルス幅に変換し、writeMicroseconds() 関数で設定することで、attach における min や max の情報の保存先の制約を回避し、有効な範囲の一部を高い解像度で指定できる。
#include <Servo.h>
Servo servo;
void setup() {
servo.attach(5); // パルス幅の範囲はデフォルトを用いる
// 0 ~ 180 の範囲で表す位置 90 を、600 us ~ 800 us のパルス幅に変換して指定する
servo.writeMicroseconds(map(90, 0, 180, 600, 800));
}
void loop() {
}
map() 関数は内部では long (32 bit) で計算を行うので、int (16 bit) の範囲の値同士であれば問題なく変換できるだろう。
さらに、この方法なら入力の範囲も自由に設定できる。
すなわち、0 ~ 180 での指定にとらわれず、0 ~ 127 などのアプリケーションで都合の良い指定方法を使うことができる。
writeMicroseconds() は、指定するパルス幅を attach() の実行時に設定した範囲内に収める補正を行う。
デフォルトの範囲 (544 ~ 2400) を超えるパルス幅を用いる場合、用いるパルス幅が全て範囲内に収まるよう、attach() で min や max を設定しておかなければならない。
当然、この際に指定する値は前述の制約を受ける。
おまけ
GitHub の Issues を見ていると、この罠の被害にあったと思われる投稿があった。
Servo write() vs writeMicroseconds() · Issue #73 · arduino-libraries/Servo
この投稿では、min を 1400、max を 1600 に設定しようとして、それぞれ 375 と 2620 付近に設定されてしまったと主張している。
1400 は 1059 超であり、1600 は 1889 未満なので、いずれの値も正常に記録できる範囲の外である。
min に関しては、-214 (0xff2a) を記録しようとして 42 (0x2a) を記録し、これは 376 を指定したとき記録される値である。
max に関しては、200 (0xc8) を記録しようとして -56 (0xc8) を記録し、これは 2624 を指定したとき記録される値である。
これらの値は、観測されたと主張している値とだいたい一致している。
まとめ
Arduino の Servo ライブラリの attach() 関数で min と max を指定する場合、min の値は 33 ~ 1059 の範囲、max の値は 1889 ~ 2915 の範囲でないと、内部でオーバーフローを起こし、予期せぬ結果を生む。
この範囲を超える下限や上限を扱いたい場合、map() などを用いてアプリケーション側でパルス幅を求め、writeMicroseconds() で直接指定することで実現できることがある。
ただし、writeMicroseconds() で設定できるパルス幅は、attach() の実行時に設定した範囲内のみに限られる。

