0. 目的と前提
Arduino Uno + nRF24L01 + 左右2個ESCで動作しているスキッドステア型Roverを、段階的にROS2構成へ移行する。 今回もCodexを利用しバイブコーディングメインで開発を行う。(本当に完成度が高く便利です。)
今回のPhaseではESCとArduinoをいったん外し、Raspberry Pi(3B)のGPIOからL298Nを直接制御する方式とした。PC側のROS2 teleopから /cmd_vel を送信し、Wi-Fi LAN経由でRasPi側ROS2ノードが受信して左右DCモータをPWM制御する。将来的にはESC制御に戻す予定。
環境制約(家にある機材の流用)からPC側はROS2 Jazzy(Ubuntu24.04)、RasPi側はROS2 Humble(Ubuntu22.04)とし、同一環境内でROS2 distributionを混載しない。Humble/Jazzy間の通信互換性を優先し、初期Phaseでは標準メッセージ geometry_msgs/msg/Twist のみを使う。
目標構成:
PC ROS2 Jazzy
rover_operator
teleop / test publisher / limiter / monitor
-> /cmd_vel geometry_msgs/msg/Twist
-> Wi-Fi LAN
Raspberry Pi ROS2 Humble
rover_base
-> GPIO direction + PWM
-> L298N
-> 左右DCモータ
単体テストではL298N経由の始動にduty 0.45以上が必要だったため、始動時だけ start_boost_duty=0.55 を使い、エンコーダtick検出後は run_min_duty=0.32 からの低速走行プロファイルへ落とす。通信断時は約0.5秒で自動停止する。
1. 実装済みパッケージ構成
PC側Jazzy用:
pc_rover_ws/
src/
rover_operator/
package.xml
setup.py
rover_operator/
cmd_vel_limiter.py
cmd_vel_test_publisher.py
topic_monitor.py
launch/
teleop_rover.launch.py
monitor_rover.launch.py
config/
teleop_limits.yaml
README.md
PC側の役割
cmd_vel_test_publisher.py: 低速テスト用の/cmd_velをpublishする。cmd_vel_limiter.py:/cmd_vel_rawを受け、速度上限をかけて/cmd_velへpublishする。topic_monitor.py:/cmd_velの受信状況をログ表示する。将来/wheel_ticksや/odom監視を追加する置き場。teleop_rover.launch.py: PC側速度リミッタと、標準teleop_twist_keyboardを起動する。
RasPi側Humble用:
raspi_rover_ws/
src/
rover_base/
package.xml
setup.py
rover_base/
l298n_driver.py
diff_drive_mixer.py
l298n_cmd_vel_node.py
encoder_counter.py
scripts/
l298n_gpio_test.py
l298n_pwm_sweep.py
encoder_count_test.py
launch/
l298n_rover.launch.py
config/
l298n_rover.yaml
test/
test_diff_drive_mixer.py
README.md
RasPi側の役割:
l298n_driver.py: GPIO方向ピンとPWMピンを直接扱う低レベルドライバ。diff_drive_mixer.py:/cmd_velから左右モータ指令への変換ロジック。l298n_cmd_vel_node.py:/cmd_vel購読、タイムアウト監視、安全停止。encoder_counter.py: フォトインタラプタ式エンコーダの左右カウント。scripts/l298n_gpio_test.py: ROS2を使わないGPIO単体テスト。scripts/l298n_pwm_sweep.py: 低速PWM始動しきい値の確認。scripts/encoder_count_test.py: エンコーダカウント確認。
2. /cmd_vel から左右モータPWMへの変換仕様
入力は geometry_msgs/msg/Twist とする。
linear.x: 前後方向。正で前進、負で後退。angular.z: 旋回方向。ROS標準に合わせ、正を左旋回、負を右旋回とする。
現在のArduino 2ESC版では、ジョイスティック値から T と S を作り、通常時に次の差動ミキシングを行っている。
L = T + k_mix * S
R = T - k_mix * S
ROS2版でもこの考え方を踏襲する。ただし入力はジョイスティック生値ではなく /cmd_vel なので、まず正規化する。
T = clamp(linear.x / max_linear, -1.0, 1.0)
S = clamp(angular.z / max_angular, -1.0, 1.0)
if abs(T) < pivot_linear_threshold and abs(S) >= pivot_angular_threshold:
if pivot_mode == "one_wheel":
if S > 0:
L = k_pivot * S
R = 0
else:
L = 0
R = -k_pivot * S
else:
L = k_pivot * S
R = -k_pivot * S
else:
L = T + k_mix * S
R = T - k_mix * S
L = clamp(L, -1.0, 1.0)
R = clamp(R, -1.0, 1.0)
初期パラメータ:
| パラメータ | 初期値 | 意味 |
|---|---|---|
max_linear | 0.30 | /cmd_vel.linear.x を100%指令とみなす値 |
max_angular | 1.50 | /cmd_vel.angular.z を100%指令とみなす値 |
k_mix | 0.25 | 通常旋回時の旋回混合率。キーボード操作向けに弱める |
k_pivot | 0.25 | ピボット時の混合率。大きく回りすぎないよう弱める |
pivot_mode | one_wheel | ピボット時は左右逆回転ではなく片輪のみ駆動する |
pivot_linear_threshold | 0.10 | ほぼ停止中とみなす前後指令 |
pivot_angular_threshold | 0.60 | その場旋回へ入る旋回指令。意図しないピボットを減らす |
deadband | 0.05 | 微小指令を停止扱いにする幅 |
invert_angular | true | 現車体で旋回方向を合わせるため角速度符号を反転 |
left_gain / right_gain | 1.00 | 左右dutyの手動補正 |
min_command | 0.05 | 正規化後の左右指令がこれ未満なら停止扱い |
start_boost_duty | 0.55 | 停止状態から動かすときの始動duty |
start_boost_sec | 0.20 | エンコーダがない場合の時間ベースboost期間 |
run_max_duty | 0.50 | 始動後の通常走行上限 |
run_min_duty | 0.32 | エンコーダtick検出後の最低走行duty |
accel_step | 0.04 | 制御周期ごとのduty増加上限 |
brake_step | 0.12 | 停止・減速側のduty変化上限 |
L298NではESCのような1000-2000usのサーボパルスではなく、方向ピンとPWM dutyで制御する。
cmd > 0:
IN_FORWARD = HIGH
IN_REVERSE = LOW
PWM = start_boost_duty until encoder ticks increase,
then map(abs(cmd), min_command..1.0, run_min_duty..run_max_duty)
cmd < 0:
IN_FORWARD = LOW
IN_REVERSE = HIGH
PWM = start_boost_duty until encoder ticks increase,
then map(abs(cmd), min_command..1.0, run_min_duty..run_max_duty)
abs(cmd) < min_command or cmd == 0:
IN_FORWARD = LOW
IN_REVERSE = LOW
PWM = 0
左右のモータ取り付け向きで前後が逆になる可能性が高いので、invert_left と invert_right をパラメータ化している。
キーボード操作では旋回量が大きくなりやすいため、現在の初期設定では k_mix=0.25, k_pivot=0.25, pivot_angular_threshold=0.60 として旋回を弱める。さらにピボット時は pivot_mode=one_wheel とし、左右逆回転ではなく片輪のみを駆動する。i で直進しない場合は、エンコーダ同期制御の前段階として left_gain/right_gain を0.05刻みで手動調整する。
3. L298N GPIOピン割り当て
BCM番号で定義する。物理ピン番号ではない。
| L298N端子 | RasPi BCM | 用途 |
|---|---|---|
IN1 | 18 | 左モータ方向A |
IN2 | 17 | 左モータ方向B |
ENA | 13 | 左モータPWM |
IN3 | 23 | 右モータ方向A |
IN4 | 22 | 右モータ方向B |
ENB | 12 | 右モータPWM |
エンコーダ入力:
| 信号 | RasPi BCM | 用途 |
|---|---|---|
ENC_L | 5 | 左フォトインタラプタ入力 |
ENC_R | 6 | 右フォトインタラプタ入力 |
L298Nの注意点:
- RasPi GNDとL298N GNDは必ず共通化する。
- L298Nのモータ電源はRasPiから取らない。
- L298Nの5V端子をRasPi 5Vへ逆給電しない構成にする。
- 初回テストは車体を浮かせ、タイヤが空転できる状態で行う。
4. 安全停止仕様
安全停止はRasPi側 rover_base の最終責務とする。PC側の速度リミッタは補助であり、RasPi側のtimeout停止を置き換えない。
- ノード起動直後は必ず停止状態。
/cmd_velを受信するまでモータは動かさない。- 最後の
/cmd_vel受信からtimeout_sec=0.5秒を超えたら停止。 linear.x=0かつangular.z=0を受信したら停止方向へ制御。- Ctrl+C、ROS shutdown、例外発生時はPWMを0、方向ピンをLOWへ戻す。
- 始動時は
start_boost_duty=0.55とし、単体テストで確認した始動duty 0.45を少し超える範囲に抑える。 - エンコーダtick検出後は
run_min_duty..run_max_dutyへ落とし、キーボード操作時の移動量を抑える。 start_boost_duty=0.55でも弱い場合は、コード変更より先にモータ電源電圧、L298Nの電圧降下、配線抵抗、タイヤ負荷を確認する。- dutyの急変を避けるため、加速側は遅く、停止・減速側は速くする。
停止時のL298N出力はフリーラン停止を初期仕様にする。
IN1/IN2 = LOW/LOW
IN3/IN4 = LOW/LOW
ENA/ENB PWM = 0
ブレーキ停止 HIGH/HIGH はモータ・L298Nへの負荷が増えるため、初期Phaseでは使わない。
5. 最初の単体テスト手順
ROS2化の前に、必ずRasPi単体でGPIOとL298Nを確認する。
cd ~/raspi_rover_ws/src/rover_base
python3 scripts/l298n_gpio_test.py --motor left --direction forward --duty 0.25 --duration 1.0
python3 scripts/l298n_gpio_test.py --motor left --direction reverse --duty 0.25 --duration 1.0
python3 scripts/l298n_gpio_test.py --motor right --direction forward --duty 0.25 --duration 1.0
python3 scripts/l298n_gpio_test.py --motor right --direction reverse --duty 0.25 --duration 1.0
PWM低速確認:
python3 scripts/l298n_pwm_sweep.py --motor left --start 0.30 --end 0.65 --step 0.05
python3 scripts/l298n_pwm_sweep.py --motor right --start 0.30 --end 0.65 --step 0.05
エンコーダ確認:
python3 scripts/encoder_count_test.py --left-pin 5 --right-pin 6
確認内容:
forwardで左右とも車体前進方向に回るか。- 左右どちらかが逆なら
invert_left/invert_rightで補正する。 - duty 45%付近で始動することを確認し、左右差があれば実測値を記録する。
- エンコーダは左右countが増えることをまず確認する。
6. PC-RasPi間ROS2通信確認手順
RasPi側:
cd ~/raspi_rover_ws
source /opt/ros/humble/setup.bash
colcon build --symlink-install
source install/setup.bash
export ROS_DOMAIN_ID=30
ros2 launch rover_base l298n_rover.launch.py
PC側:
cd ~/pc_rover_ws
source /opt/ros/jazzy/setup.bash
colcon build --symlink-install
source install/setup.bash
export ROS_DOMAIN_ID=30
PCから低速テスト:
ros2 run rover_operator cmd_vel_test_publisher --ros-args \
-p topic:=/cmd_vel \
-p linear_x:=0.05 \
-p angular_z:=0.0 \
-p duration_sec:=3.0
PCからteleop:
ros2 launch rover_operator teleop_rover.launch.py
teleop_rover.launch.py はxterm内で標準teleop_twist_keyboardを起動し、/cmd_vel_rawへpublishする。cmd_vel_limiterが速度上限をかけて/cmd_velへ中継する。VNCでは、開いたxtermをクリックして操作する。
PC側で標準teleopとxtermを設定する。
sudo apt install ros-jazzy-teleop-twist-keyboard xterm
iは前進、,は後退、j/lはその場旋回、u/oは前進旋回、m/.は後退旋回である。標準teleopはキーごとに1つの指令を送るため、iの後にj/lを押すと前進は維持されずその場旋回に切り替わる。
通信確認で見ること:
- PCから
ros2 topic listで/cmd_velが見えるか。 - RasPi側ノードログに受信時刻と変換後の左右dutyが出るか。
- publishを止めて0.5秒程度で停止するか。
teleop_twist_keyboardでkまたは未割当キーを押したとき、停止指令またはtimeoutで止まるか。
ネットワークで見えない場合の確認:
- PCとRasPiが同じLANにいるか。
ROS_DOMAIN_IDが一致しているか。- firewallがDDS通信を遮っていないか。
geometry_msgs/msg/Twistの標準型だけで試しているか。
7. ESC x2 / Pixhawk / ArduPilot / MAVLinkへの拡張方針
今回のL298N制御を最終形にしない。ROS2上の入力と足回り出力の境界を分け、後からESCやPixhawkへ戻せる構成にする。
上位ROS2インターフェースは維持する。
/cmd_vel geometry_msgs/msg/Twist
下位出力だけを差し替える。
DriveBackend
- L298NBackend: GPIO direction + PWM
- EscServoBackend: 50Hz, 1000-2000us pulse
- PixhawkMavlinkBackend: MAVLink MANUAL_CONTROL / RC override / setpoint
ESC x2へ戻す場合:
- 中立: 1500us
- 前進: 1500-2000us
- 後退: 1500-1000us
- 左右個別に
neutral_usを調整可能にする。 - ESC初期化・キャリブレーションモードは通常走行ノードと分ける。
- L298Nのduty制御とは別物として扱い、同じPWM関数を流用しない。
Pixhawk / ArduPilot / MAVLink連携:
- RasPiがGPIOを直接叩く代わりに、ROS2ノードからMAVLinkへ指令を渡す。
mavrosまたはMAVLink Pythonライブラリを候補にする。- failsafeはPixhawk/ArduPilot側にも持たせ、ROS2ノード側timeoutと二重化する。
- L298N PhaseではMAVLink依存を入れない。
8. エンコーダによる自立走行支援
フォトインタラプタ式エンコーダは各モータ軸に取り付け済みで、スリット穴は20個。将来の自立走行では左右速度のフィードバック、走行距離推定、オドメトリ生成に使う。
このPhaseでは閉ループ制御には入れず、まずカウント確認と始動判定ログまでに留める。
- 左右エンコーダの立ち上がり/立ち下がりをカウント。
- 一定周期で左右count、差分count、推定回転数をログ出力。
- モータ指令とカウント増減の向きを確認する。
/cmd_velで非ゼロdutyを出した後、startup_check_sec=0.20秒以内にstartup_min_ticks=1以上増えたら始動済みとしてログ出力する。- tickが増えない場合は未始動警告を出し、始動boost dutyを維持する。
- tick検出後は通常走行状態として
run_min_duty..run_max_dutyの低速走行プロファイルに移行する。 - 左右回転同期補正やPID制御はこのPhaseでは行わない。
将来のROS2インターフェース:
/wheel_ticks: 左右累積tickをpublish。/wheel_speed: 左右車輪速度をpublish。/odom:nav_msgs/msg/Odometryをpublish。tf:odom -> base_linkをpublish。
最初の閉ループ制御は左右速度差の補正から始める。車輪径、トレッド幅、ギヤ比、スリット数、エッジ数をパラメータ化する。
9. Phase完了条件
- RasPi単体GPIOテストで左右モータの前進/後退/停止が確認できる。
- duty 45-60%の範囲で低速走行できる。
- RasPi内の
ros2 topic pubで/cmd_vel走行確認ができる。 - PC JazzyからRasPi Humbleへ
/cmd_velを送って動作する。 teleop_twist_keyboardで前進、後退、左右旋回、その場旋回ができる。- publish停止またはWi-Fi断相当で0.5秒程度以内に停止する。
- 左右反転、旋回方向、PWM上限をパラメータで調整できる。
- エンコーダは少なくとも左右カウント確認まで実施できる。

