LDROBOT D300 LiDARキットの調査
ずっと前にLIDAR D300を購入してほったらかし状態でしたが、Picoマイコンで制御できないか調査してみることにしました。直近のゴールとしては、Picoでデータを拾ってROS2にパブリッシュすることを考えています。
D300について
2万円ぐらい?購入した当時は13000円ぐらいだった気がする。
なんでもかんでも値上がりしてます。。2万円まで来ると、ちょっと手出しづらい気がする。
D300ってのはキットの名前なのか?機種としてはLD19なんですかね。。不明。仕様は以下で良いと思ってます。
キットの内容
キットに入っていたもの。本体とUSB接続するための基盤、ケーブルです。D300の特徴としてはとても小さいのが売りなんですかね?性能は安価なほかのLIDARと同レベルです。あんまり期待はしないほうがいい。
D300のI/Oインタフェース
本体のI/Oを確認しておきます。
pin | signal | min[V] | max[V] | note |
---|---|---|---|---|
1 | Tx | 0 | 3.3 | シリアルデータ出力、Baud rate=230400、受信側のRXに接続 |
2 | PWM | 0 | 3.3 | 回転速度制御、30KHz推奨 |
3 | GND | 0 | 0 | GND |
4 | VDD | 4.5 | 5.5 | 5V電源 |
シリアル通信についてはOUTPUTのみです。受信側のUART RXに接続しておくと、距離データが流れてくるんだと思います。
D300の速度制御(pin2)について
公式を翻訳して要約すると
- このピンがGNDに接地されていると、内部速度制御で10Hzの速度になる
- 外部から制御する場合はPWMの矩形波を送信し、Duty比で速度制御する
- 外部PWMの場合、20k ~ 50K程度の周期で、推奨は30kHz
- Duty比は45~55%の間で、最低でも100ms連続入力する
- 外部制御がトリガーされると、電源切るまでずっと外部制御モードになる
外部制御のトリガーがイマイチよく分からないけど
- まず100ms程度Duty比50%で開始すると、外部制御モードになる。(電源オフまで)
- それ以降は0~100%で制御できる
ってことかもしれない。これは後程試す必要がある。
D300キットの基板
ICが2個付いてました。片方はCP2102なので、一般的なUSBシリアル変換ICで、これでUSB経由でシリアルデータが読み取れます。問題はもう片方のICで、どうもARMマイコンっぽい??何やっているのか謎です。
速度制御のPWM波形を作成しているのかもしれないので、信号をオシロスコープで見たところ、矩形波が出てました。
電圧 | 3.3V |
周期 | 40us=25kHz程度 |
Duty比 | 30/40 = 75% |
ちなみにPWMをGNDに繋いで実際に回転させてみても違いが良く分からなかったです。上記の条件だと同じ回転速度なのかもしれない。ほかにD300のデータを加工したりしてるかもしれないですが、不明です。
D300の出力データ
シリアルデータ構造
公式を翻訳した内容です。以下の順番にデータが詰まっている模様。
値 | サイズ | 内容 |
---|---|---|
Header | 1Byte | 値はデータパケットの先頭を示す0x54 固定値 |
VerLen | 1Byte | 上位3ビットはパケットタイプを示し、現在は1 固定、下位5ビットはパケット内の測定点数を示し現在12 固定であるため、Byte値は0x2C固定となる |
Speed | 2Byte | 単位は度/秒で、ライダーの速度を示す |
Start angle | 2Byte | 単位は0.01度で、データパケット点の開始角度 |
Data | 3Byte | 測定データ ※下記参照 |
End angle | 2Byte | 単位は0.01度で、データパケット点の終了角度 |
Timestamp | 2Byte | 単位はミリ秒、最大値は30000カウント |
CRC check | 1Byte | 自分自身を除くすべての前のデータの検証値 |
測定データ(3Bytes)
合計3バイトのデータ内訳は、2バイトの距離値と1バイトの信号強度値で構成される。
- 距離値の単位はmm
- 信号強度値は光の反射強度を反映する。強度が高いほど信号強度値は大きく、強度が低いほど信号強度値は小さくなります。6m以内の白い物体の場合、信号強度値は200程度。
データのサンプル例
各点の角度値は、開始角度と終了角度の線形補間によって求められる。角度の計算方法は以下の通りである:
フィールド | 値 | 例 |
---|---|---|
header | 54 | 54 固定 |
ver_len | 2C | 12 固定 |
speed(LSB) | 68 | 0868 = 2152 deg/sec |
speed(MSB) | 08 | |
start_angle(LSB) | AB | 7EAB = 32427 = 324.27deg |
start_angle(MSB) | 7E | |
data | 以下 | |
end_angle(LSB) | BE | 82BE = 33470 = 334.7deg |
end_angle(MSB) | 82 | |
timestamp(LSB) | 3A | 1A3A = 6714 = 6714ms |
timestamp(MSB) | 1A | |
crc8 | 50 |
dataフィールド | 値 | 例 |
---|---|---|
distance1(LSB) | E0 | 00E0 = 224mm |
distance1(MSB) | 00 | |
intensity1 | E4 | 228 |
distance2(LSB) | DC | 00DC = 200mm |
distance2(MSB) | 00 | |
intensity2 | E2 | 226 |
: | : | |
distance12(LSB) | B0 | 00B0 = 176mm |
distance12(MSB) | 00 | |
intensity12 | EA | 234 |
D300の開始・終了角度
どこを基準にしているかというと、下図のように▲印のところが0degらしい。
接地に影響するので注意。-90degの方向を正面とするか、▲印に合わせるべきか。
ROS2のLIDARメッセージフォーマット
D300のデータフォーマットが分かったので、ついでにROS2のLIDARメッセージのフォーマットも確認します。
ROS2にもあらかじめ用意されたLIDAR用のメッセージフォーマットがあります。2DタイプのLIDARであれば、sensor_msgs/msg/LaserScanのフォーマットが用意されているので、これに合わせると可視化などやりやすい。
フィールド | 型 | 内容 | 例 |
---|---|---|---|
header | std_msgs/Header | タイムスタンプと座標フレームID | 例)<BR>header.frame_id = "laser_frame"<BR>header.stamp = self.get_clock().now().to_msg() |
angle_min | float32 | スキャンの開始角度(ラジアン) | -1.57 (-90°) |
angle_max | float32 | スキャンの終了角度(ラジアン) | 1.57 (90°) |
angle_increment | float32 | 各スキャンポイント間の角度差(ラジアン) | 0.01 (約0.57°) |
time_increment | float32 | 1スキャンポイント間の時間差(秒) | 0.0 |
scan_time | float32 | 1回のスキャンにかかる総時間(秒) | 1.0 |
range_min | float32 | 測定可能な最小距離(メートル) | 0.1 |
range_max | float32 | 測定可能な最大距離(メートル) | 10.0 |
ranges | float32[] | 距離データの配列(メートル) | [1.2, 1.5, inf, 0.9] |
intensities | float32[] | 各ポイントの反射強度データ | [100, 150, 0, 90] |
ここでいう1スキャンというのは、機種によって仕様が異なるが、1パケット分のデータだったり、1回転分のデータのことを指す。D300の場合12個のデータ固定なので、これが1スキャン分のパブリッシュデータとなる。
D300の出力するデータであれば上記のフィールドは埋めることできそうです。
参考)D300のデータのサンプルコード
データフォーマット(C言語)
#define POINT_PER_PACK 12
#define HEADER 0x54
typedef struct __attribute__((packed)) {
uint16_t distance;
uint8_t intensity;
} LidarPointStructDef;
typedef struct __attribute__((packed)) {
uint8_t header;
uint8_t ver_len;
uint16_t speed;
uint16_t start_angle;
LidarPointStructDef point[POINT_PER_PACK];
uint16_t end_angle;
uint16_t timestamp;
uint8_t crc8;
} LiDARFrameTypeDef;
サンプルにあったCRCのチェック方法
static const uint8_t CrcTable[256] = {
0x00, 0x4d, 0x9a, 0xd7, 0x79, 0x34, 0xe3, 0xae, 0xf2, 0xbf, 0x68, 0x25,
0x8b, 0xc6, 0x11, 0x5c, 0xa9, 0xe4, 0x33, 0x7e, 0xd0, 0x9d, 0x4a, 0x07,
0x5b, 0x16, 0xc1, 0x8c, 0x22, 0x6f, 0xb8, 0xf5, 0x1f, 0x52, 0x85, 0xc8,
0x66, 0x2b, 0xfc, 0xb1, 0xed, 0xa0, 0x77, 0x3a, 0x94, 0xd9, 0x0e, 0x43,
0xb6, 0xfb, 0x2c, 0x61, 0xcf, 0x82, 0x55, 0x18, 0x44, 0x09, 0xde, 0x93,
0x3d, 0x70, 0xa7, 0xea, 0x3e, 0x73, 0xa4, 0xe9, 0x47, 0x0a, 0xdd, 0x90,
0xcc, 0x81, 0x56, 0x1b, 0xb5, 0xf8, 0x2f, 0x62, 0x97, 0xda, 0x0d, 0x40,
0xee, 0xa3, 0x74, 0x39, 0x65, 0x28, 0xff, 0xb2, 0x1c, 0x51, 0x86, 0xcb,
0x21, 0x6c, 0xbb, 0xf6, 0x58, 0x15, 0xc2, 0x8f, 0xd3, 0x9e, 0x49, 0x04,
0xaa, 0xe7, 0x30, 0x7d, 0x88, 0xc5, 0x12, 0x5f, 0xf1, 0xbc, 0x6b, 0x26,
0x7a, 0x37, 0xe0, 0xad, 0x03, 0x4e, 0x99, 0xd4, 0x7c, 0x31, 0xe6, 0xab,
0x05, 0x48, 0x9f, 0xd2, 0x8e, 0xc3, 0x14, 0x59, 0xf7, 0xba, 0x6d, 0x20,
0xd5, 0x98, 0x4f, 0x02, 0xac, 0xe1, 0x36, 0x7b, 0x27, 0x6a, 0xbd, 0xf0,
0x5e, 0x13, 0xc4, 0x89, 0x63, 0x2e, 0xf9, 0xb4, 0x1a, 0x57, 0x80, 0xcd,
0x91, 0xdc, 0x0b, 0x46, 0xe8, 0xa5, 0x72, 0x3f, 0xca, 0x87, 0x50, 0x1d,
0xb3, 0xfe, 0x29, 0x64, 0x38, 0x75, 0xa2, 0xef, 0x41, 0x0c, 0xdb, 0x96,
0x42, 0x0f, 0xd8, 0x95, 0x3b, 0x76, 0xa1, 0xec, 0xb0, 0xfd, 0x2a, 0x67,
0xc9, 0x84, 0x53, 0x1e, 0xeb, 0xa6, 0x71, 0x3c, 0x92, 0xdf, 0x08, 0x45,
0x19, 0x54, 0x83, 0xce, 0x60, 0x2d, 0xfa, 0xb7, 0x5d, 0x10, 0xc7, 0x8a,
0x24, 0x69, 0xbe, 0xf3, 0xaf, 0xe2, 0x35, 0x78, 0xd6, 0x9b, 0x4c, 0x01,
0xf4, 0xb9, 0x6e, 0x23, 0x8d, 0xc0, 0x17, 0x5a, 0x06, 0x4b, 0x9c, 0xd1,
0x7f, 0x32, 0xe5, 0xa8};
uint8_t CalCRC8(const uint8_t *data, uint16_t data_len) {
uint8_t crc = 0;
while (data_len--) {
crc = CrcTable[(crc ^ *data) & 0xff];
data++;
}
return crc;
}
上記のコードは、以下から抜粋しただけです。
LD06、LD19に対応したROS2のドライバ
ちなみにPicoのPWM出力について
そういえばということで、PicoでPWM波形出力どうやってやるのかついでに調査。
もし、PicoにD300をシリアル接続する場合、上記の変換基板は使いたくないです。(そもそもUSB使わないので)
Picoに変換基板の仕事の一部をやってもらうとして、PWMピンに矩形波を渡すケースも念頭に入れます。(最悪GNDに設置しても動くけど)
PicoのPWMモジュール、スライス、チャンネルについて
Raspberry Pi PicoのPWMモジュールについて、スライス、チャンネルというキーワードが出てきます。
- スライスごとにTimerカウンタが独立していて、周波数を個別に指定できる
- チャンネルABは共通カウンタなのでDuty比だけ個別に指定できる
スライスについては、PWM0-PWM7まで合計8個ある。GPIOに割り当てられているものを見ると、一部スライスが被っているものもあるので注意。
PWM0 から PWM3
GPIOピン | PWMスライス | チャンネル |
---|---|---|
GPIO 0 | PWM 0 | A |
GPIO 1 | PWM 0 | B |
GPIO 2 | PWM 1 | A |
GPIO 3 | PWM 1 | B |
GPIO 4 | PWM 2 | A |
GPIO 5 | PWM 2 | B |
GPIO 6 | PWM 3 | A |
GPIO 7 | PWM 3 | B |
PWM4 から PWM7
GPIOピン | PWMスライス | チャンネル |
---|---|---|
GPIO 8 | PWM 4 | A |
GPIO 9 | PWM 4 | B |
GPIO 10 | PWM 5 | A |
GPIO 11 | PWM 5 | B |
GPIO 12 | PWM 6 | A |
GPIO 13 | PWM 6 | B |
GPIO 14 | PWM 7 | A |
GPIO 15 | PWM 7 | B |
上記と被っているもの
GPIOピン | PWMスライス | チャンネル |
---|---|---|
GPIO 16 | PWM 0 | A |
GPIO 17 | PWM 0 | B |
GPIO 18 | PWM 1 | A |
GPIO 19 | PWM 1 | B |
GPIO 20 | PWM 2 | A |
GPIO 21 | PWM 2 | B |
GPIO 22 | PWM 3 | A |
GPIO 23 | PWM 3 | B |
GPIO 24 | PWM 4 | A |
GPIO 25 | PWM 4 | B |
GPIO 26 | PWM 5 | A |
GPIO 27 | PWM 5 | B |
GPIO 28 | PWM 6 | A |
GPIO 29 | PWM 6 | B |
以下は、GPIO0(PWM0 A)で1kHz、デューティサイクル50%の矩形波を生成するサンプル。
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#define PWM_PIN 0 // 使用するGPIOピン (GPIO 0)
int main() {
// 標準入出力を初期化
stdio_init_all();
// GPIOピンをPWM機能に設定
gpio_set_function(PWM_PIN, GPIO_FUNC_PWM);
// ピンに対応するPWMスライス番号を取得
uint slice_num = pwm_gpio_to_slice_num(PWM_PIN);
// デフォルトのPWM周波数を変更(例えば1kHz)
pwm_set_clkdiv(slice_num, 4.0f); // クロック分周設定
// デューティサイクルの範囲を設定(0-65535)
pwm_set_wrap(slice_num, 65535);
// デューティサイクル50%に設定
pwm_set_chan_level(slice_num, PWM_CHAN_A, 32768);
// PWM出力を開始
pwm_set_enabled(slice_num, true);
while (true) {
sleep_ms(1000);
}
}
ネタはそろったので、次回以降で、PicoにD300を接続して制御、距離データを拾うところまでやってみる予定です。
Discussion