工学男子の日常

モノづくりが好きな男子の日記です。

STM32でAS5047とかのCPHA=1(SPIモード1)なSPIデバイスをDMA使って最速で読み出す

検索に引っかかるようにと思ってタイトルが長くなってしまいました。


STM32のSPIではDMA+ハードウェアNSSを使うことでCPU負荷無しで連続読み出しが可能です。ただしこれはCPOL=0かつCPHA=0、いわゆるSPIモード0のデバイスのみに限られます。謎仕様
(SSピンを常時LOWにすることで連続読み書きが可能なデバイスもあるが、AS5047はじめ読み書きごとにSSをHIGHにする必要があるデバイスも多い)


CPHA=1のデバイスについても、工夫することでCPU負荷無しかつハードウェアNSSより高速に読み出すことができたのでその紹介になります。


ちなみにAS5047は磁気エンコーダーで、14bit出力で最大2万8千rpmに対応した非常に高速なデバイスです。動的角度補償機能を内蔵しており応答速度も非常に速く、絶対角の出力が可能なので、今後光学エンコーダーからこういう磁気エンコーダーへの置き換えが進みそうだなぁと言う感じです。

特にここ最近ロボコン界隈ではロボマスモーター終売がもっぱらの噂なので、モタドラ自作勢も増えると予想され、この記事の需要もそれなりにあるといいなぁ。

実際の通信

10MHzなのでデジタル味が消滅している。
毎秒50万回読み出せています。
一回16bitで8Mbps=帯域8割使い切ってる。えらい。

仕組み

今回の方法の肝はNSS(CSn)をPWMとしてタイマで制御し、DMAをCircularモードにして連続的にデータの読み書きを行うことです。

処理の流れは以下図のようになります。

  1. PWMによりCSnがHIGH(=デバイスが非選択状態)になる
  2. タイマのカウント値(CNT)がコンペア値(CCR1)を超えてCSnがLOW(=デバイスが選択状態)になる
  3. それと同時にタイマに紐づいたDMA1が起動しメモリからSPIペリフェラルのデータレジスタへ書き込みを行う
  4. MOSIからデータが送信されると同時にMISOから受信したデータがSPIペリフェラルのデータレジスタに格納される
  5. 格納が完了するとSPI受信に紐づいたDMA2が起動しデータレジスタの値をメモリに書き込む
  6. タイマのカウント値(CNT)が上限値(ARR)に達し1.に戻る

つまり(ARR-CCR1)の値はSPIの通信速度とビット数に応じて適切な時間になるように手動で設定する必要があります。

CubeMXの設定

今回はSTM32G431、SPIはSPI1、タイマはTIM8のCH1を使用しました。


タイマの設定は以下のようになります。
要点はPWMがHIGH→LOWの順になるようにCounter Mode=Up、PWM Mode1にすることです。
タイミングについてはタイマに供給するクロック速度160MHz(=6.25ns/cycle)に対して、AS5047のデータシートよりSS=HIGH時間が350ns必要なのでCCR1=56cycle、通信速度10MHzで16bit転送するのに必要な1600ns+350nsでARR=320cycleとしています。

タイマのDMAの設定は以下のようになります。
TIM8_CH1を選択すること、Circularモードにすること、PeripheralのIncremental Addressのチェックを外すこと、Peripheral To Memoryを選択すること(実際はメモリ→SPIの書き込みだが、起動がTIM側なのでこうなるらしい)を忘れないでください。
今回は16bitで書き込むのでData WidthはHalf Wordを選択します。


続いてSPIの設定は以下のようになります。
Prescalarを16に設定して通信速度を10MHzにします。

SPIのDMAの設定は以下のようになります。
SPI側では受信時のみDMAを起動するのでSPI1_RXを選択します。こちらはSPIからメモリへ書き込むのでPeripheral To Memoryを選択します。
ちなみにTIMもSPIも割り込みはDMA以外すべてOFFで大丈夫です(HAL_SPI_TxRxCpltCallbackとかを使わないので)。

プログラム

送受信を開始する関数は以下になります。
実行後は自動で連続的にSPI通信が行われ、最新の値がpRxDataに格納されます。

void MC_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, const uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, TIM_HandleTypeDef *htim)
{
	  DMA_HandleTypeDef *hdma_tim = htim->hdma[TIM_DMA_ID_CC1];	//TimerのCC1に紐づいたDMA
	  DMA_HandleTypeDef *hdma_spi = hspi->hdmarx;			//SPI RXに紐づいたDMA

	  SET_BIT(hspi->Instance->CR2, SPI_CR2_RXDMAEN);	//SPI受信時DMA起動有効化
	  __HAL_TIM_ENABLE_DMA(htim, TIM_DMA_CC1);		//TIM CC1 DMA有効化

	  HAL_DMA_Start(hdma_tim,(uint32_t)pTxData,(uint32_t)&(hspi->Instance->DR),Size);
	  HAL_DMA_Start(hdma_spi,(uint32_t)&(hspi->Instance->DR),(uint32_t)pRxData,Size);

	  __HAL_SPI_ENABLE(hspi);	//SPI有効化
	  HAL_TIM_PWM_Start(htim,TIM_CHANNEL_1);
}

STM32ではよく使う処理はHAL関数で準備されているのですが、今回はすこしイレギュラー(タイマを基準にSPIレジスタへ書き込む)なことをしているので直接レジスタをいじります。

SPI_CR2_RXDMAENはSPIの受信完了時にSPI_RXに結びついたDMAを起動させるという設定のbitです。
タイマでもDIERというレジスタでCC1到達時にDMAを起動させる設定をするのですが、こちらはマクロがあったのでマクロで有効化しています。

HAL_DMA_Start()関数で送受信先を指定します。メモリ→ペリフェラルでもペリフェラル→メモリでも送信元が先で送信先が後になるようにしてください。
SPIではhspi->Instance->DRに送信内容が書き込まれるとクロックが開始し、受信内容が同じレジスタに書き込まれます。

今回はpTxDataにはデバイスの読み出したいアドレスである0x3FFFが入った変数のポインタを格納します。

最後にマクロでSPIを有効化してPWMを起動します。
ここでHAL_TIM_PWM_Start_DMA()とかHAL_SPI_TransmitReceive_DMA()とかするとレジスタがリセットされるのでマクロで有効化します。






記事の内容は以上になります。
DMAの使い方はタイマ自身のARRを書き換えたりもできるのでいろいろと応用が効きそうで、勉強になりました。
閲覧いただきありがとうございました。

クレジットガイダンスのパスワードがわからなくなったので総当たり攻撃で開けた話

2024年11月からCICの信用情報開示で自分の信用スコアであるクレジットガイダンスが閲覧できるようになりました。

自分も来春から社会人ということで気になって開示したのですが、閲覧後にPDFのパスワードがわからなくなってしまいました。そこでツールを用いて総当たり攻撃をかけ、無事開くことができたのでその備忘録です。


※自分の所有するファイルなので違法性はありません。ツール自体もコマンドラインで開けるまで順番に試しているだけなので、自分で当てずっぽうにたくさん入力してたまたま開くことができた場合となんら変わりません。

パスワードの規則性

規則性というか決まり方なのですが、6桁の受付番号と決済に使ったクレジットカードの有効期限の4桁をくっつけた合計10桁になっています。
自分の信用スコアは?CICの「クレジット・ガイダンス」開始、開示請求方法を解説 - 日本経済新聞
つまり後ろ4桁はカードを見ればわかるので実際に総当たりするのは数字6桁で済むことになります。(決済をクレジットカード以外でした人はよくわかりません)

ツールのインストール

今回は以下のツールを使います。

これらのツールを動かすために各自Linux環境をご準備ください。自分はWSLを使いました。
learn.microsoft.com

以下コマンドでツールをインストールします。

$ sudo apt install pdfcrack
$ sudo apt install crunch

辞書ファイルの生成

実はpdfcrackだけでも解析は可能です。以下のコマンドで数字のみ、10桁で総当たりを行うことができます。

$ pdfcrack -c 0123456789 -n 10 ファイル名

手元の環境では5万ワード毎秒くらいのペースで解析が進んでいました。しかし0~9の10文字10桁をこのペースで総当りすると10^10/50000で最悪20万秒=56時間かかります。
そこで前述の規則性をもとに辞書ファイルを作成することで20秒以下まで高速化することができます。

まずcrunchを用いて000000から999999までの6桁の数をすべて書き出します。

$ crunch 6 6 0123456789 -o passlist.txt

さらにsedコマンドを用いてクレジットカードの有効期限の4桁を末尾に追加します。ここでは例として2031年02月の場合のコマンドをのせます。

$ sed 's/$/0231/' passlist.txt -i

これでpasslist.txtには0000000231から9999990231のすべての候補が書き出されました。

解析の実行

以下コマンドで作成したリストから総当たり攻撃を行います。

$ pdfcrack -w passlist.txt ファイル名

15秒ほどでパスワードを発見することができました。


記事の内容は以上になります。
本当はpdfcrackに正規表現などの機能があればよかったんですがなかったのでこんな感じになりました。今回は規則性を便利に利用しましたが、逆に少ない桁数や、数字のみのパスワードや辞書に載っている単語をつかったパスワードの危険性が実感できました。

【ROS2】CartographerとNavigation2でお手軽経路生成する話

これの続きです。
kogakudanshi.hatenablog.jp

$ sudo apt install ros-humble-navigation2 ros-humble-nav2-bringup

launchファイルを編集

前回の記事でつくった~/ros2_ws/src/bringup/launch/bringup.launch.pyを書き換えます。

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from launch.substitutions import PathJoinSubstitution

def generate_launch_description():
    
    ldlidar_launch = IncludeLaunchDescription(
        launch_description_source=PythonLaunchDescriptionSource([
            get_package_share_directory('ldlidar_node'),
            '/launch/ldlidar_with_mgr.launch.py'
        ])
    )

    laser_filter_node = Node(
            package="laser_filters",
            executable="scan_to_scan_filter_chain",
            parameters=[
                    PathJoinSubstitution([
                    get_package_share_directory("laser_filters"),
                    "examples", "box_filter_example.yaml",
            ])],
            remappings=[('/scan','/ldlidar_node/scan'),('/scan_filtered','/scan')],
    )

    tf2_base_link_ldlidar_base = Node(
        package='tf2_ros',
        executable='static_transform_publisher',
        arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'ldlidar_base']
    )

    bringup_dir = get_package_share_directory('bringup')
    cartographer_config_dir = os.path.join(bringup_dir, 'config')
    configuration_basename = 'noimu_lds_2d.lua'

    use_sim_time = False
    resolution = '0.05'
    publish_period_sec = '1.0'

    cartographer_node = Node(
            package='cartographer_ros',
            executable='cartographer_node',
            name='cartographer_node',
            output='screen',
            parameters=[{'use_sim_time': use_sim_time}],
            arguments=['-configuration_directory', cartographer_config_dir,
                       '-configuration_basename', configuration_basename]
    )

    occupancy_grid_node = Node(
            package='cartographer_ros',
            executable='cartographer_occupancy_grid_node',
            name='cartographer_occupancy_grid_node',
            output='screen',
            parameters=[{'use_sim_time': use_sim_time}],
            arguments=['-resolution', resolution,
                        '-publish_period_sec', publish_period_sec]
    )

    #navigation2の起動
    nav2_launch = IncludeLaunchDescription(
        launch_description_source=PythonLaunchDescriptionSource([
            get_package_share_directory('nav2_bringup'),
            '/launch/navigation_launch.py'
        ]),
        launch_arguments={
                'use_sim_time': 'False',
        }.items()
    )

    rviz2_config = os.path.join(
        get_package_share_directory('bringup'),
        'rviz',
        'config.rviz'
    )

    rviz2_node = Node(
        package='rviz2',
        executable='rviz2',
        name='rviz2',
        output='screen',
        arguments=[["-d"], [rviz2_config]],
        remappings=[('/move_base_simple/goal','/goal_pose')] #目標位置姿勢を示すTopic
    )

    ld = LaunchDescription()
    ld.add_action(ldlidar_launch)
    ld.add_action(laser_filter_node)
    ld.add_action(tf2_base_link_ldlidar_base)
    ld.add_action(cartographer_node)
    ld.add_action(occupancy_grid_node)
    ld.add_action(nav2_launch)
    ld.add_action(rviz2_node)
    return ld

Nav2のlaunch descriptionとrviz2のremappingを追加しました。

ビルドして実行

$ cd ~/ros2_ws
$ colcon build --symlink-install --packages-select bringup
$ source install/setup.bash
$ ros2 launch bringup bringup.launch.py

Rvizではまだ表示設定が完了していないので以下設定をします。

  1. Navigationグループにチェック→loacal及びglobalコストマップが表示される
  2. Add->By topicから/goal_poseを追加→Rvizの2D Goal Poseボタンで設定した位置姿勢が表示される
  3. Add->By topicから/planを追加→Nav2が生成した経路パスが表示される

設定したらCtrl+Sで保存しておきます。

2D Goal Poseボタンで目標位置姿勢を指示します。

Nav2で生成された経路

生成された経路が表示されました。

またロボットに渡される速度情報である/cmd_velトピックを見てみます。

$ ros2 topic echo /cmd_vel
linear:
  x: 0.26
  y: 0.0
  z: 0.0
angular:
  x: 0.0
  y: 0.0
  z: 0.1578947368421052
---
linear:
  x: 0.23263157894736847
  y: 0.0
  z: 0.0
angular:
  x: 0.0
  y: 0.0
  z: 0.1578947368421052
---
...

適切に出力されています。あとはこれをサブスクライブして適当な通信手段でマイコンに送信するノードを作れば(めんどい)、自律移動ロボットの完成です。もしくはros2_ontrolを使えば差動二輪、3輪オムニなどの形式ごとにモーター速度まで計算してくれるはずです(もっとめんどい)。


ちなみにIMUやOdometryなしのLiDAR一本足SLAMだと結構頻繁に位置推定が地平の彼方へと吹っ飛んでいきます。


あと現状コストマップが生成されていない(LiDARが届いていないなど)エリアは立入禁止エリアと認識されています。
qiita.com
起動直後だとコストマップが生成されていない箇所は結構あって不便ですので、こちらのサイトを参考にtrack_unknown_spaceとかをいい感じに設定するといい感じだと思います。


今回の記事だと簡略化のためにビヘイビアツリー(bt)やパラメータファイル、フレーム変換等は全く扱いませんでした。本格的に自律移動したい場合はそのへんをガリガリ記述していくことになるんだと思います。
記事の内容は以上になります。


ご覧いただきありがとうございました。

【ROS2】Cartographerを使ってLiDARの出力だけからSLAMする話

これの続きです。
kogakudanshi.hatenablog.jp


こちらの記事を参考にGazeboから実際のLiDARに置き換えたものになります。
qiita.com

パッケージのインストール

$ sudo apt install ros-humble-cartographer
$ sudo apt install ros-humble-cartographer-rviz
$ sudo apt install ros-humble-laser-filters #/scanトピックの変換のため

起動用パッケージの作成

全部のノードを手動で開いてもいいんですが、流石にターミナルの枚数が多くなりすぎるので起動専用のパッケージを作成します。本当は依存関係を記述するとrosdepで勝手にインストールまでやってくれると思うんですが、難しくてやめました。

$ cd ~/ros2_ws/src
$ ros2 pkg create bringup
$ cd bringup
$ mkdir rviz
$ touch rviz/config.rviz
$ mkdir launch
$ touch launch/bringup.launch.py
$ mkdir config
$ touch config/noimu_lds_2d.lua

最終的な~/ros2_ws/bringup/のディレクトリ構成は以下になります。

~/ros2_ws/src/bringup$ tree
.
├── CMakeLists.txt
├── config
│   └── noimu_lds_2d.lua
├── include
│   └── bringup
├── launch
│   └── bringup.launch.py
├── package.xml
├── rviz
│   └── config.rviz
└── src

6 directories, 5 files

~/ros2_ws/src/bringup/CMakeList.txtの最後に以下を追記します。

install(
  DIRECTORY launch rviz config
  DESTINATION share/${PROJECT_NAME}/
)

~/ros2_ws/src/bringup/launch/bringup.launch.pyに以下を記述します。

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from launch.substitutions import PathJoinSubstitution

def generate_launch_description():
    
    ldlidar_launch = IncludeLaunchDescription(
        launch_description_source=PythonLaunchDescriptionSource([
            get_package_share_directory('ldlidar_node'),
            '/launch/ldlidar_with_mgr.launch.py'
        ])
    )

    laser_filter_node = Node(
            package="laser_filters",
            executable="scan_to_scan_filter_chain",
            parameters=[
                    PathJoinSubstitution([
                    get_package_share_directory("laser_filters"),
                    "examples", "box_filter_example.yaml",
            ])],
            remappings=[('/scan','/ldlidar_node/scan'),('/scan_filtered','/scan')],
    )

    tf2_base_link_ldlidar_base = Node(
        package='tf2_ros',
        executable='static_transform_publisher',
        arguments=['0', '0', '0', '0', '0', '0', 'base_link', 'ldlidar_base']
    )

    bringup_dir = get_package_share_directory('bringup')
    cartographer_config_dir = os.path.join(bringup_dir, 'config')
    configuration_basename = 'noimu_lds_2d.lua'

    use_sim_time = False
    resolution = '0.05'
    publish_period_sec = '1.0'

    cartographer_node = Node(
            package='cartographer_ros',
            executable='cartographer_node',
            name='cartographer_node',
            output='screen',
            parameters=[{'use_sim_time': use_sim_time}],
            arguments=['-configuration_directory', cartographer_config_dir,
                       '-configuration_basename', configuration_basename]
    )

    occupancy_grid_node = Node(
            package='cartographer_ros',
            executable='cartographer_occupancy_grid_node',
            name='cartographer_occupancy_grid_node',
            output='screen',
            parameters=[{'use_sim_time': use_sim_time}],
            arguments=['-resolution', resolution, '-publish_period_sec', publish_period_sec]
    )

    rviz2_config = os.path.join(
        get_package_share_directory('bringup'),
        'rviz',
        'config.rviz'
    )

    rviz2_node = Node(
        package='rviz2',
        executable='rviz2',
        name='rviz2',
        output='screen',
        arguments=[["-d"], [rviz2_config]]
    )

    ld = LaunchDescription()
    ld.add_action(ldlidar_launch)
    ld.add_action(laser_filter_node)
    ld.add_action(tf2_base_link_ldlidar_base)
    ld.add_action(cartographer_node)
    ld.add_action(occupancy_grid_node)
    ld.add_action(rviz2_node)
    return ld

~/ros2_ws/src/bringup/rviz/config.rvizには以下のファイルの内容をコピペします。Rvizの設定ファイルは自分で書くと大変なので。
github.com
~/ros2_ws/src/bringup/config/noimu_lds_2d.luaに以下を記述します。

include "map_builder.lua"
include "trajectory_builder.lua"

options = {
  map_builder = MAP_BUILDER,
  trajectory_builder = TRAJECTORY_BUILDER,
  map_frame = "map",
  tracking_frame = "base_link", --"imu_link",
  published_frame = "base_link", --"odom",
  odom_frame = "odom",
  provide_odom_frame = true, --false,
  publish_frame_projected_to_2d = true,
  use_odometry = false, --true,
  use_nav_sat = false,
  use_landmarks = false,
  num_laser_scans = 1,
  num_multi_echo_laser_scans = 0,
  num_subdivisions_per_laser_scan = 1,
  num_point_clouds = 0,
  lookup_transform_timeout_sec = 0.2,
  submap_publish_period_sec = 0.3,
  pose_publish_period_sec = 5e-3,
  trajectory_publish_period_sec = 30e-3,
  rangefinder_sampling_ratio = 1.,
  odometry_sampling_ratio = 1.,
  fixed_frame_pose_sampling_ratio = 1.,
  imu_sampling_ratio = 1.,
  landmarks_sampling_ratio = 1.,
}

MAP_BUILDER.use_trajectory_builder_2d = true

TRAJECTORY_BUILDER_2D.min_range = 1.0 --0.12
TRAJECTORY_BUILDER_2D.max_range = 8.0 --3.5
TRAJECTORY_BUILDER_2D.missing_data_ray_length = 3.
TRAJECTORY_BUILDER_2D.use_imu_data = false
TRAJECTORY_BUILDER_2D.use_online_correlative_scan_matching = true 
TRAJECTORY_BUILDER_2D.motion_filter.max_angle_radians = math.rad(0.1)

POSE_GRAPH.constraint_builder.min_score = 0.65
POSE_GRAPH.constraint_builder.global_localization_min_score = 0.7

--POSE_GRAPH.optimize_every_n_nodes = 0

TRAJECTORY_BUILDER.pure_localization_trimmer = {
    max_submaps_to_keep = 3,
}
POSE_GRAPH.optimize_every_n_nodes = 20

return options

ROS2でcartographerを使う手順(1) SLAMからPure Localizationまで #Cartographer - Qiita
Cartographer 全パラメータ&意訳コメント #ROS - Qiita
これらを参考にLD19向けに変更したものです。

ビルドします。

$ cd ~/ros2_ws
$ colcon build --symlink-install --packages-select bringup

実行

$ source install/setup.bash
$ ros2 launch bringup bringup.launch.py


地図の生成と自己位置推定が動作しました。

別のウィンドウで開いたターミナルからrqtグラフを表示してみます

$ rqt_graph


本当はlaser_filtersはいらないんですが、ldlidarの/scanトピックからnamespaceを外すことができなかったため仕方なく挟んでます。Cartographerのparamから直接/ldlidar_node/scanを読み込む設定もあったので気になる方はそちらをいじってみてください。

記事の内容は以上になります。
ご覧いただきありがとうございました。

続き
kogakudanshi.hatenablog.jp

【ROS2】激安で買ったLD19 LiDARを使う話

購入

LD19

Switch ScienceとかAmazonとかで買えます。1~2万円くらい。
自分はAliexpressでなぜか1800円で売っているのを発見し、まさかと思って購入したらちゃんと届きました。送料込み2400円。
ちなみにCOIN-D4っていう製品、全く同じものだと思うんだけど買った人いたらコメントください。
2024年ごろは確かに同じ製品写真だったと思うんだけど、最近その名称のまま明らかに違う製品が売られてます。ただし送料別2千円台と格安。公式SDKはあって有志のros2対応もあるので使った人いたら報告ください。ちなみにLD19もSTL-19PかD300に改名した?っぽい。ミニLiDAR情勢は複雑怪奇。

環境

記事中ではWSL2上のUbuntu 22.04 LTSを使います。
そのほか

  • x86_64のPCにインストールしたUbuntu MATE 22.04 LTS
  • ArmのCPUが載ったSBC(Raspberry Pi 3B+, Orange Pi 5)にインストールしたUbuntu 22.04 LTS

でもほぼ同様のやり方でインストールできました。WSL2だとPC外からROS2ノードが見えないので、移動ロボット等に積む予定の方は外付けSSDなどにUbuntuをインストールして使うのがおすすめです。
USBシリアル変換モジュールを介してLD19をUSBに接続しておきます。配線は以下。

LD19側 モジュール側
5V 5V
GND GND
TX RX

WSL2にUbuntuをインストール(Windowsのみ)

コマンドプロンプトで使用できるディストリビューションを確認

wsl --list --online

Ubuntu 22.04をインストール

wsl --install -d Ubuntu-22.04

通常のLinuxインストールと同様にUsernameとPasswordの設定を求められます。

usbipdの有効化(Windowsのみ)

このままだとPCに挿入したLiDARデバイスをWSL側から認識できないのでusbipdを設定します。以下のサイトに従ってusbipdをインストールします。
learn.microsoft.com
コマンドプロンプトを管理者権限で実行します。

usbipd list

一覧の中からシリアル変換モジュールと思われるデバイスのBUSIDを控えます。今回は1-3でした。

usbipd bind --busid 1-3
usbipd attach --wsl --busid 1-3

※これ以降のコマンドはすべてWSL上での操作になります。

接続確認する

$ ls /dev/ttyUSB*
$ /dev/ttyUSB0

WSL2では大丈夫でしたが、シリアル変換ICにCH341などのCH340系を使用しているとLinux側から認識できないことがあります。
dmesgコマンドを実行して抜き差しするとbrlttyというソフトが奪っていることがわかります。これは視覚障がい者用入力デバイスソフトらしいのですが、CH341のプロダクトIDがそのまま使われていることが原因と思われます。不要な場合は以下のコマンドでアンインストールすれば解消します。

$ sudo apt-get remove brltty

また読み書きに必要な権限の設定が悪い場合は

$ sudo chmod a+rw /dev/ttyUSB0

で権限を追加するか、自動実行などを設定する予定がある方は以下を参考にデフォルトルールを変更してください。
qiita.com

ROS2のインストール

ROS2はLinuxのバージョンごとに使えるバージョンが決まっています。22.04ではHumbleです。ROS2のインストールについてはいろいろ記事があるのでそちらを参照してください。
qiita.com↑わかりやすかった(”Gazeboのインストール”については今回は不要)

ldrobot-lidar-ros2のビルド準備

ワークスペースとソースディレクトリの作成(ワークスペースの作成が済んでいる場合は不要)

$ mkdir ~/ros2_ws
$ cd ~/ros2_ws
$ mkdir src
$ cd src

リポジトリをクローン、必要なパッケージのインストール

$ git clone https://github.com/Myzhar/ldrobot-lidar-ros2.git
$ sudo apt install libudev-dev python3-rosdep2

公式ではここでudevルールの設定をしていますが、CP210x用のIDが使われているため今回使っているCH341では動きません。ルールを使っているシリアル変換ICに応じて書き換えます。

$ nano src/ldrobot-lidar-ros2/rules/ldlidar.rules

idVendorとidProductはdmesgに接続したときの履歴が残っているはずなのでそちらを参照してください。

# set the udev rule , make the device_port be fixed by ldlidar
# CH341 USB Device
KERNEL=="ttyUSB*", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", MODE:="0777", SYMLINK+="ldlidar"

書き換えたら反映させます。

$ cd ~/ros2_ws/src/ldrobot-lidar-ros2/scripts/
$ ./create_udev_rules.sh


もしくは、このあと別のシリアル変換モジュールをつなぐ予定の方や、同じ変換ICが載ったArduinoなどをつなぐ予定のある方はudevを書き換えずにパラメータファイルを

$ nano src/ldrobot-lidar-ros2/ldlidar_node/params/ldlidar.yaml
    comm:
      serial_port: 'dev/ttyUSB0' #'/dev/ldlidar'

などと書き換えて使ってください。

ビルド

$ cd ~/ros2_ws #必ずワークスペースディレクトリに移動する
$ rosdep update
$ rosdep install --from-paths src --ignore-src -r -y
$ colcon build --symlink-install --cmake-args=-DCMAKE_BUILD_TYPE=Release

起動

ライフサイクルマネージャーと別々に起動している例が多いですが、確認だけなので同時に立ち上げるlaunchファイルを使います。

$ source install/setup.bash
$ ros2 launch ldlidar_node ldlidar_with_mgr.launch.py

別のコマンドプロンプトを立ち上げてWSLにログインします。

$ ros2 topic list
/bond
/diagnostics
/joint_states
/ldlidar_node/scan
/ldlidar_node/transition_event
/parameter_events
/robot_description
/rosout
/tf
/tf_static

/ldlidar_node/scanトピックがパブリッシュされているのが確認できます。Rviz2を立ち上げます。

$ rviz2

まだmap->ldlidar_baseのフレームが提供されていないのでとりあえずFixed Frameをmap以外に変更します。
Addボタンからメニューを開きBy topicを確認するとLaserScan型のトピックがあるので追加します。

Rviz2でLaserScanを確認

動作しているのが確認できました。
ターミナルからCtrl+Cで終了します。

$ ros2 launch ldlidar_node ldlidar_rviz2.launch.py
$ ros2 launch ldlidar_node ldlidar_slam.launch.py

とかの起動ファイルもありますので試してみてください。

ご覧いただきありがとうございました。


続き
kogakudanshi.hatenablog.jp

CBF1000ST SC64(CBF1000F?)納車の経緯

CBF1000ST納車しました!!

CBF1000ST

……って言っても購入したのは4月で納車は5月半ばなんですけどね。

しかしまぁこれが変なバイクでして、一応型番はSC64とはっきりしているんですが正式な車名であるCBF1000STと検索しても情報がほとんど出てきません。というのもこいつは逆輸入車、いわゆる逆車でして製造はイタリアです。どうもCBF1000Fと検索したほうが出るっぽい?キャラクター的に国内でヒットするようなモデルでもないので我が国では絶賛不人気車となっているわけです。

今回はそんなバイクを納車した経緯報告になります。

 

思えば3月も終わりごろ、馴染のレッ◯バロンでVTRの整備待ちをしていた私は展示スペースで1台のバイクに目が止まりました。

XJ6 Diversion Fです。なんですかそれ。まぁヤマハ600ccフルカウル4気筒のありがちっちゃありがちなバイクです。欧州免許制度に合わせてFZ6の馬力を下げたモデルです。国内ではフェザーの方ばっかり売れたので超不人気車。ただし低走行車庫保管だったのでピカピカ。

 

「車検を通したら並びますよ。たぶん50万切るんじゃないかな。」安い。自分がVTRを買った価格プラス10万です。試しにエンジンを掛けてもらうと、デチューンで低回転のトルクが上がっている分レーサーのような吹け上がりです(ただし3千回転まで)。

この瞬間に、前年に大型免許を取得していて、9万キロを超えたVTRの調子が悪化しつつあり、さらに就職を来年に控えた僕に「大型を買う」という選択肢が現実的なものとして降ってきたのでした。

 

また数週間後、展示スペースでXJ6を眺めていると隣のバイクにも目が止まりました。600ccにして98馬力、センターアップマフラーを備えたFZ6とフルパニアのBandit 1250F。どちらも60万を切る価格でした。これは迷う。特に残り1年で長距離ツーリングの計画をいろいろ立てていた自分にとってバンディットは魅力的でした。

 

そして1週間後再度お店に向かうと…無い!!聞けばバンディットは前日に売れてしまったとのことでした。ここから混迷のバイク選びが始まります。考えているうちにフルパニアがどうしても欲しくなった自分は、初代トレーサーを射程に入れるため予算を70万まで上げることを閣議決定しました(←バカ)。がしかし、都合よく見つかるわけもなく店員さんに印刷してもらった在庫を前に唸るばかり。しかしながら指針は得ました。「人気はなくても自分の用途にあったマイナー車を探すべし、そうバンディットのような…」

 

 

帰宅後、Webikeのわがままバイク選びを開き条件を打ち込みます。

・大型、FI、2000年以降2020年以前の車種

・国内メーカー

・400ccに負けるのは悔しい、50馬力以上

・ビクスク、競技用車、SS、アメリカン除く

そしてこれを人気が低い順に並び替えます。

 

ホンダ/ヤマハ/スズキ/カワサキ(2000~2020年式)の生産終了のバイクをスペック、足つき、車両重量、燃費などで比較する|ウェビック バイク選び

 

出ましたでました。
人気がない順に(←失礼)ホーネット900、CBF1000(前期・後期)/600、VFR1200、そしてフェザー、XJ6、Bandit 1200/1250…。これこそ自分が知りたかった情報です!

早速これを持ってバイク屋に駆け込み、在庫を検索してもらいます。そしたらありました。フルパニア、ETC、グリップヒーター付きのCBF1000F。60万円以下。僕は握りしめた現金20万円を机に叩きつけて叫びました。「これください!!」

 

 

 

というのが納車の経緯です。その後無事納車され、GWに大間経由で北東北を一周するツーリングに出かけたりなどしましたが、(燃費を除いて)素晴らしいバイクです。
その模様は僕の5月ごろのタイムラインをご参照ください(丸投げ)。

 

本当はスクリーン交換の話を書くつもりだったんですが、長くなったので次回にします。6月中はNHKロボコンや別のロボコンで全く乗るチャンスがなかったので、今月中に北海道に行って取り返す予定なのでご期待ください。

ともかく不人気車ランキングに乗るくらいの不人気車なので、レビューもなければパーツもない、整備ブログなんてもってのほか。外人ニキのYoutubeを見ながらいじっていますが、いいバイクだということが伝わるようになるべく情報を載せていきたいですね。

 

ご覧いただきありがとうございました。

倒立振子を線形二次レギュレーター(LQR)で”簡単に”立たせる話

 

kogakudanshi.hatenablog.jp

前回でシミュレーターができたのでやっと制御できます。
自分で問題を作って自分で解く、虚無ですね。

 

振り子を立たせるだけでなく、台車位置もゼロに保つようにしてみたいと思います。入力は台車にはたらく力とします。つまり制御対象の状態変数が複数あるのに対して入力は1つの劣駆動システムになります。

運動方程式の線形化と状態方程式・観測方程式

求めた運動方程式

\left \lbrace \begin{array}{l}(M+m)\ddot{x}+\frac{lm}{2}\cos{\theta}\ddot{\theta}-\frac{lm}{2}\sin{\theta}\dot{\theta}^2=F\\ \cos{\theta}\ddot{x}+\frac{2l}{3}\ddot{\theta}-g\sin{\theta}=0\end{array}\right.

棒の傾きが微小の時\theta\rightarrow0を仮定して線形化します。

\ddot{x}=-\frac{3mg}{4M+m}\theta+\frac{4}{4M+m}F

\ddot{\theta}=\frac{6\left(M+m\right)g}{\left(4M+m\right)l}\theta-\frac{6}{\left(4M+m\right)l}F

状態変数を定義します。

x=\begin{bmatrix} x \\ \theta \\ \dot{x} \\ \dot{\theta}\end{bmatrix}

上の線形化した式を用いて状態方程式を作ります。

 

\dot{x}=Ax+Bu

また観測方程式は以下のようになります。シミュレーターなので全ての状態変数が観測できるものとします。

これでシステムが把握できたので制御を組んでいきます。

線形二次レギュレーター

LQRでは以下の評価関数を最小化するゲインKを求めます。

J=\int_0^\infty(x^T(t)Qx(t)+u^T(t)Ru(t))dt

Qは状態に関する重み行列、Rは入力に関する重み行列です。ざっくりとしたイメージでいうと状態変数xの各成分が0から離れていて、その時間が長いほどx^T(t)Qx(t)の値が大きくなります。また入力uが0から離れていて、その時間が長いほどu^T(t)Ru(t)の値が大きくなります。

また対角行列であるQの各行が状態変数の各成分の重みになっているので、優先して収束させたい状態変数のと同じ行の対角成分を大きくすることで過渡特性をチューニングすることができます。またQに比べてRの値を大きくすると多少収束に時間がかかってもいいので入力を減らせ、という省エネ命令になるわけです。

 

入力は以下のように状態変数にゲインをかけた値になります。

u=-Kx

つまりこのKをうまく決めてやるとJがより小さくなる、つまり目標値から外れている時間が短くなり、またより少ない入力でそれを達成できるわけです。

最適ゲインの算出

それじゃあKはどのように決めたらいいのか……というふうに話が進むわけですが「簡単に」と銘打っているように、ここでは自分では求めません。当たり前です。

 

具体的にはmatlabにおんぶに抱っこします。

m = 1;
M = 4;
l = 4;
g = 9.8;

A = [0 0 1 0; 0 0 0 1; 0 -3*m*g/(4*M+m) 0 0; 0 6*(M+m)*g/(4*M+m)/l 0 0]
B = [0; 0; 4/(4*M+m); -6/(4*M+m)/l]
Q = eye(4)
R = 1

lqr(A,B,Q,R)

はい出ました。

簡単ですね。

 

4つの数に左から順にx,\theta,\dot{x},\dot{\theta}をかけた値の合計を求め、-1をかけた値を制御入力Fとします。前回作ったシミュレーターに制御入力を加えた結果が以下です。

これに対して先程のmatlabのシステムに同じ初期値を与えたときの応答が以下です。

上から一番目と二番目、x,\thetaのグラフはほぼ一致していて、どちらも15秒前後で静定していることがわかると思います。一方で\dot{\theta}はまだ誤差が残っています。もう少し動かすとゼロにはなりますが、制御モデルが線形化されているのに対してシミュレーターが非線形であるためと考えられます。

より高速にxをゼロにするためQの1行1列目を1から5にしてみます。

気持ち収束が早くなった気がします。その代わり移動中の棒の傾きは大きくなってしまいました。

次にQ単位行列に戻して、制御量を少なくするためRを1から10にしてみます。

台車の加減速がゆっくりになっているのがわかると思います。制御量が少なくなってエコ&動かしているアクチュエータには優しくなりましたが、静定時間は伸びてしまいました。

 

以上のように最適制御は一つのQ,Rに対しては計算によってパラメーターが一意に決まりますが、理想とする動きに近づけるためにはチューニングが必要です。最適であることが保証されているからこそ、すべての性能がトレードオフになっています。

このような難しさもありますが、重み行列をとりあえず単位行列にしておけば動くし多入力、劣駆動システム対応の制御法としては一番簡単だと思います。

 

ここまでご覧いただきありがとうございました。
記事中に間違いなどありましたらお気軽にコメントで教えていただけると助かります。