🔦

LDROBOT D300 LiDARキットの調査

2025/02/08に公開

ずっと前にLIDAR D300を購入してほったらかし状態でしたが、Picoマイコンで制御できないか調査してみることにしました。直近のゴールとしては、Picoでデータを拾ってROS2にパブリッシュすることを考えています。

D300について

2万円ぐらい?購入した当時は13000円ぐらいだった気がする。
なんでもかんでも値上がりしてます。。2万円まで来ると、ちょっと手出しづらい気がする。

https://jp.robotshop.com/products/ldrobot-d300-lidar-kit

D300ってのはキットの名前なのか?機種としてはLD19なんですかね。。不明。仕様は以下で良いと思ってます。
https://wiki.youyeetoo.com/en/Lidar/D300

キットの内容

キットに入っていたもの。本体と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連続入力する
  • 外部制御がトリガーされると、電源切るまでずっと外部制御モードになる

外部制御のトリガーがイマイチよく分からないけど

  1. まず100ms程度Duty比50%で開始すると、外部制御モードになる。(電源オフまで)
  2. それ以降は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のフォーマットが用意されているので、これに合わせると可視化などやりやすい。

https://docs.ros.org/en/noetic/api/sensor_msgs/html/msg/LaserScan.html

フィールド 内容
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のドライバ
https://github.com/ldrobotSensorTeam/ldlidar_stl_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