TL;DR
- SO-101はAI(VLAなどの基盤モデルやACTなどの模倣学習)の指示からモーター内部の制御によって指定した位置に移動している。
- 指示通りの位置へ移動できることは外乱、構造、制御の特性の影響があるので当たり前ではない。
- SO-101はAIが指示した位置にアームが完全に移動しきれていないことがある。
- 現在のモーター制御は実質的なPD制御(I=0)であり、アームを伸ばした時など重力下で定常偏差が発生する。
- 解決策として、特に重力の影響が大きいモーターにIゲイン(積分要素)を導入し、モーターごとの負荷に合わせたPID制御の最適化を検討した。
- LeRobotのコードはSTS3215の使い方として疑問な部分がある。
- record.pyなどを実行するたびに、EEPROMへの書き込みが発生している。
- read onlyなはずのアドレスへの書き込みがある。
- サーボモーターの加速度・速度プロファイルについて、設定すれば台形速度プロファイルが実現できるが、現在の設定では目標速度に達する前に減速が始まってしまい、速度プロファイルが三角形状になる。これにより、動作の開始時と停止時に急な躍度(ジャーク)発生の可能性がある。
- モーター制御をいじるかどうかは目的しだい。
はじめに
こんにちは。ABEJAの道辻です。ABEJAではプロジェクトマネージメントを中心に色々やっておりますが、前職までは15年くらい新商品開発、新規開発をおこなう部署で機械設計、研究をしていたという経歴です。
2年前に記事を書いてから久しぶりに書きますが、AIとロボットがより近づいてきました。 tech-blog.abeja.asia
ロボットアームそのものを作ったことはありませんが、モーターを使ったシステムや制御設計などもしていたので、その視点でSO-101について検証しようと思います。 LeRobotのハッカソンに参加させてもらって、実際にACTで模倣学習を行いましたが、カクつくような動きが見られたため、PIDのゲインを調整しています。 様々なAIモデルを試すとともに、モーター側の制御についても理解を深めておいた方がいいのではないかと考えています。
この記事では、SO-101の制御システムを整理し、PIDの調整結果について紹介しようと思います。
SO-101のモーター制御概要
SO-101の制御は、Pythonスクリプトから、モーター(STS3215)内部のコントローラーへと、複数の階層を経て実行されます。
AI(VLAなどの基盤モデルやACTなどの模倣学習)の出力を受け取り、{'shoulder_pan.pos': 30}のようなPythonの辞書(action)に変換します。 actionを受け取り、正規化された値(-100~100など)を、モーター固有の動作範囲(例:500~3500)にマッピングします。 例えば、action=30という指示は2450というモーター固有の値に変換されます。 全6モーターへの指示値をSYNC_WRITEプロトコルで一つのパケットにまとめ、シリアル通信で一括送信します。 そのパケットを受け取り、モーター内部のPIDコントローラーが目標位置と現在位置の差を計算し、PWMでモーターを駆動します。

なので、厳密には今使われているAIはEnd to Endではないと考えています。計算時間のかかるAIの計算とその目標ポジションへ移動させる制御が別々になっているのは、制御周期的に理にかなっているのかなと思いますがこの辺りも今後工夫されるポイントだと予想しています。
SO-101のコードからEEPROMとSRAMに何を書き込んでいるか
STS3215サーボモーターには、特性の異なる2つのメモリが搭載されています。
- EEPROM: 電源を切っても内容が保持される不揮発性メモリです。PIDゲインやIDなど、頻繁には変更しない定数的なパラメータの書き込みに使用されます。ただし、書き込み回数に10万回などの上限があるため、頻繁な書き換えは避けるべきです。意図は分かりませんが現在の
record.pyなどでは実行のたびに書き込みが行われています。10万回レベルまで行くことはないと思いますが、読み取って書き込もうとしてる値が同じだったら書き込み処理を行わないなどは最低限やってもいいのかと思います。 - SRAM: 電源が切れると内容が消える揮発性メモリです。目標位置(ゴールポジション)など、動作中にリアルタイムで更新する動作指示の書き込みに使用されます。
基本的にはSRAMのTarget PositionにAIからの指示が書き込まれて、Current Positionとの差分を使って、EEPROMに書き込まれているゲインでPID制御がされています。
STS3215のメモリーテーブル
| DEC | HEX | デバッグツールの表示名 | Area | R/W | 説明 |
|---|---|---|---|---|---|
| 0 | 0x0 | Firmware Main Version | EPROM | r | ファームウェアのメジャーバージョン番号。 |
| 1 | 0x1 | Firmware Secondary Ver. | EPROM | r | ファームウェアのマイナーバージョン番号。 |
| 2 | 0x2 | Servo Main Version | EPROM | r | サーボハードウェアのメジャーバージョン番号。 |
| 3 | 0x3 | Servo Sub Version | EPROM | d | サーボハードウェアのマイナーバージョン番号。 |
| 5 | 0x5 | ID | EPROM | rw | バスのユニークな識別ID。同一バス上での重複は不可。ID 254 (0xFE) はブロードキャストIDで、応答はありません。 |
| 6 | 0x6 | Baud Rate | EPROM | rw | 0~7でボーレートを指定: 1000000, 500000, 250000, 128000, 115200, 76800, 57600, 38400 |
| 7 | 0x7 | Reserved | EPROM | - | 応答遅延時間。最小単位は2µs。最大 254 * 2 = 508µs。 |
| 8 | 0x8 | Status Return Level | EPROM | rw | 0: Read/Ping命令以外は応答しない。 1: 全ての命令に応答する。 |
| 9 | 0x9 | Min Position Limit | EPROM | rw | 動作角度の最小値。マルチターンモードでは 0 を設定。 |
| 11 | 0xB | Max Position Limit | EPROM | rw | 動作角度の最大値。マルチターンモードでは 0 を設定。 |
| 13 | 0xD | Max Temperature Limit | EPROM | rw | 内部温度の上限。設定精度は1℃。 |
| 14 | 0xE | Max Input Voltage | EPROM | rw | 入力電圧の上限。80に設定すると8.0V。設定精度は0.1V。 |
| 15 | 0xF | Min Input Voltage | EPROM | rw | 入力電圧の下限。40に設定すると4.0V。設定精度は0.1V。 |
| 16 | 0x10 | Max Torque Limit | EPROM | rw | サーボの最大出力トルク。1000 = 100%ストールトルク。起動時にこの値がTorque Limit (0x30)にコピーされます。 |
| 18 | 0x12 | Setting Byte | EPROM | rw | 特殊機能用。 |
| 19 | 0x13 | Protection Switch | EPROM | rw | 各種保護機能でトルクをオフにする条件を設定。対応するビットを1で有効化。 Bit5:過負荷, Bit4:角度, Bit3:電流, Bit2:温度, Bit1:センサー, Bit0:電圧 |
| 20 | 0x14 | LED Alarm Condition | EPROM | rw | エラー発生時にLEDを点滅させる条件を設定。対応するビットを1で有効化。 Bit5:過負荷, Bit4:角度, Bit3:電流, Bit2:温度, Bit1:センサー, Bit0:電圧 |
| 21 | 0x15 | Position P Gain | EPROM | rw | 位置制御ループのP(比例)ゲイン。 |
| 22 | 0x16 | Position D Gain | EPROM | rw | 位置制御ループのD(微分)ゲイン。 |
| 23 | 0x17 | Position I Gain | EPROM | rw | 位置制御ループのI(積分)ゲイン。 |
| 24 | 0x18 | Punch | EPROM | rw | 最小起動トルク。1000 = 100%ストールトルク。 |
| 25 | 0x19 | MAX I | EPROM | rw | 最大電流。 |
| 26 | 0x1A | CW Dead Band | EPROM | rw | 時計回り方向の不感帯(デッドゾーン)。 |
| 27 | 0x1B | CCW Dead Band | EPROM | rw | 反時計回り方向の不感帯(デッドゾーン)。 |
| 28 | 0x1C | Overload Current | EPROM | rw | 過電流保護の閾値。最大 500 * 6.5mA = 3250mA。 |
| 30 | 0x1E | Angular Resolution | EPROM | rw | 角度の分解能。この値を変更すると制御回転数を拡張できます。マルチターン制御時は Phase(0x12) の BIT4 を 1 に設定する必要があります。 |
| 31 | 0x1F | Position Offset Value | EPROM | rw | 現在位置のオフセット補正値。BIT11が正負の方向を示します。 |
| 33 | 0x21 | Work Mode | EPROM | rw | 0: 位置サーボモード 1: 定速モーターモード 2: PWM開ループモード 3: ステッピングサーボモード |
| 34 | 0x22 | Protect Torque | EPROM | rw | 過負荷保護中に維持するトルク。20に設定すると最大トルクの20%。 |
| 35 | 0x23 | Overload Protection Time | EPROM | rw | 過負荷状態が継続した場合に保護機能が作動するまでの時間。200に設定すると2秒。 |
| 36 | 0x24 | Overload Torque | EPROM | rw | 過負荷保護タイマーを開始するトルクの閾値。80に設定すると最大トルクの80%。 |
| 37 | 0x25 | Velocity P Gain | EPROM | rw | 速度制御ループのP(比例)ゲイン(モード1で使用)。 |
| 38 | 0x26 | Overcurrent Protection | EPROM | rw | 過電流保護が作動するまでの時間。最大 2540ms。 |
| 39 | 0x27 | Velocity I Gain | EPROM | rw | 速度制御ループのI(積分)ゲイン(モード1で使用)。 |
| 40 | 0x28 | Torque Enable | SRAM | rw | 0: トルクOFF, 1: トルクON, 128: 現在位置を2048として補正。 |
| 41 | 0x29 | Goal Acceleration | SRAM | rw | 加減速度。10に設定すると1000 step/s²。 |
| 42 | 0x2A | Goal Position | SRAM | rw | 目標位置。 |
| 44 | 0x2C | Goal PWM | SRAM | rw | PWMモードでの目標時間/デューティ比。範囲は50~1000。BIT10は方向ビット。 |
| 46 | 0x2E | Goal Velocity | SRAM | rw | 目標速度。50 step/s = 0.732 RPM。 |
| 48 | 0x30 | Torque Limit | SRAM | rw | リアルタイムのトルク制限値。起動時にMax Torque (0x10)の値が設定されます。 |
| 55 | 0x37 | Lock | SRAM | rw | EPROMへの書き込みロック。0: ロック解除(設定値は電源OFF後も保存) 1: ロック(設定値は電源OFF後に破棄) |
| 56 | 0x38 | Present Position | SRAM | r | 現在の位置。 |
| 58 | 0x3A | Present Velocity | SRAM | r | 現在の速度。 |
| 60 | 0x3C | Present PWM | SRAM | r | 現在の負荷。モーター駆動のデューティ比に相当。 |
| 62 | 0x3E | Present Input Voltage | SRAM | r | 現在のサーボ入力電圧。 |
| 63 | 0x3F | Present Temperature | SRAM | r | 現在のサーボ内部温度。 |
| 64 | 0x40 | Sync Write Flag | SRAM | r | 非同期書き込み命令使用時のフラグ。 |
| 65 | 0x41 | Hardware Error Status | SRAM | r | エラー状態のフィードバック。対応するビットが1でエラー発生。 Bit5:過負荷, Bit4:角度, Bit3:電流, Bit2:温度, Bit1:センサー, Bit0:電圧 |
| 66 | 0x42 | Moving Status | SRAM | r | 1: 動作中, 0: 停止中。 |
| 69 | 0x45 | Present Current | SRAM | r | 現在の消費電流。 |
| 71 | 0x47 | Goal Position 2 | SRAM | r | |
| 80 | 0x50 | Moving Threshold | DEFAULT | d | |
| 81 | 0x51 | DTs(ms) | DEFAULT | d | |
| 82 | 0x52 | Vk(ms) | DEFAULT | d | |
| 83 | 0x53 | Vmin | DEFAULT | d | |
| 84 | 0x54 | Vmax | DEFAULT | d | |
| 85 | 0x55 | Amax | DEFAULT | d | |
| 86 | 0x56 | KAcc | DEFAULT | d |
tables.pyとデバッグツールの表示名の対応
/src/lerobot/motors/feetech/tables.pyにあるSTSのマップ名称とデバッグツールの表示名の対応を整理しています。
| DEC | HEX | tables.py | デバッグツールの表示名 |
|---|---|---|---|
| 0 | 0x0 | Firmware_Major_Version | Firmware Main Version |
| 1 | 0x1 | Firmware_Minor_Version | Firmware Secondary Ver. |
| 2 | 0x2 | Model_Number | Servo Main Version |
| 3 | 0x3 | Servo Sub Version | |
| 5 | 0x5 | ID | ID |
| 6 | 0x6 | Baud_Rate | Baud Rate |
| 7 | 0x7 | Return_Delay_Time | Reserved |
| 8 | 0x8 | Response_Status_Level | Status Return Level |
| 9 | 0x9 | Min_Position_Limit | Min Position Limit |
| 11 | 0xB | Max_Position_Limit | Max Position Limit |
| 13 | 0xD | Max_Temperature_Limit | Max Temperature Limit |
| 14 | 0xE | Max_Voltage_Limit | Max Input Voltage |
| 15 | 0xF | Min_Voltage_Limit | Min Input Voltage |
| 16 | 0x10 | Max_Torque_Limit | Max Torque Limit |
| 18 | 0x12 | Phase | Setting Byte |
| 19 | 0x13 | Unloading_Condition | Protection Switch |
| 20 | 0x14 | LED_Alarm_Condition | LED Alarm Condition |
| 21 | 0x15 | P_Coefficient | Position P Gain |
| 22 | 0x16 | D_Coefficient | Position D Gain |
| 23 | 0x17 | I_Coefficient | Position I Gain |
| 24 | 0x18 | Minimum_Startup_Force | Punch |
| 25 | 0x19 | MAX I | |
| 26 | 0x1A | CW_Dead_Zone | CW Dead Band |
| 27 | 0x1B | CCW_Dead_Zone | CCW Dead Band |
| 28 | 0x1C | Protection_Current | Overload Current |
| 30 | 0x1E | Angular_Resolution | Angular Resolution |
| 31 | 0x1F | Homing_Offset | Position Offset Value |
| 33 | 0x21 | Operating_Mode | Work Mode |
| 34 | 0x22 | Protective_Torque | Protect Torque |
| 35 | 0x23 | Protection_Time | Overload Protection Time |
| 36 | 0x24 | Overload_Torque | Overload Torque |
| 37 | 0x25 | Velocity_closed_loop_ P_proportional_coefficient | Velocity P Gain |
| 38 | 0x26 | Over_Current_ Protection_Time | Overcurrent Protection |
| 39 | 0x27 | Velocity_closed_loop_ I_integral_coefficient | Velocity I Gain |
| 40 | 0x28 | Torque_Enable | Torque Enable |
| 41 | 0x29 | Acceleration | Goal Acceleration |
| 42 | 0x2A | Goal_Position | Goal Position |
| 44 | 0x2C | Goal_Time | Goal PWM |
| 46 | 0x2E | Goal_Velocity | Goal Velocity |
| 48 | 0x30 | Torque_Limit | Torque Limit |
| 55 | 0x37 | Lock | Lock |
| 56 | 0x38 | Present_Position | Present Position |
| 58 | 0x3A | Present_Velocity | Present Velocity |
| 60 | 0x3C | Present_Load | Present PWM |
| 62 | 0x3E | Present_Voltage | Present Input Voltage |
| 63 | 0x3F | Present_Temperature | Present Temperature |
| 64 | 0x40 | Sync Write Flag | |
| 65 | 0x41 | Status | Hardware Error Status |
| 66 | 0x42 | Moving | Moving Status |
| 69 | 0x45 | Present_Current | Present Current |
| 71 | 0x47 | Goal_Position_2 | Goal Position 2 |
| 80 | 0x50 | Moving_Velocity / Moving_Velocity_Threshold | Moving Threshold |
| 81 | 0x51 | DTs | DTs(ms) |
| 82 | 0x52 | Velocity_Unit_factor | Vk(ms) |
| 83 | 0x53 | Hts | Vmin |
| 84 | 0x54 | Maximum_Velocity_Limit | Vmax |
| 85 | 0x55 | Maximum_Acceleration | Amax |
| 86 | 0x56 | Acceleration_Multiplier | KAcc |
PID制御の最適化
SO-101はアームを水平に伸ばした状態でモーター2へ上方向に移動させるよう指示しても大きめに動かそうとしないと動きません。これが大きめの指示を誘発してガタガタと揺れるモードになる可能性があると考えました。本当は重力の影響を考慮した制御を考える方がいいかもしれないですが、CWとCCWをモーター側で変える方法が見つからなかったので、とりあえずPI制御で目的の位置に移動するようにしました。
PID制御とは?
この問題を理解し、解決するために、まずはサーボモーターで使われているPID制御について簡単に説明します。
PID制御は、目標値に素早く正確に到達させるための、最も古典的で広く使われているフィードバック制御技術です。「目標位置」と「現在位置」の偏差(ズレ)を継続的に監視し、その偏差をゼロにするようにモーターの動きを賢く調整します。
P・I・Dはそれぞれ以下の3つの異なる制御動作の頭文字を取っており、これらを組み合わせることで理想的な動きを目指します。
P (Proportional) - 比例制御
役割: 「現在の偏差の大きさ」に比例して動作します。目標から遠いほど、大きな力で動かそうとします。ロボットアームを動かすための最も基本的な力です。
課題: これだけだと、重力などの持続的な負荷に負けて、目標位置にわずかに届かない「定常偏差」が残ることがあります。
I (Integral) - 積分制御
役割: 「過去からの偏差の蓄積」に比例して動作します。時間が経っても残っている定常偏差を解消するために、じわじわと力を追加していきます。重力で垂れ下がったアームを、目標位置まで持ち上げるような働きをします。
課題: 力を加えすぎる傾向があるため、目標を通り越してしまう「オーバーシュート」が発生しやすくなります。
D (Differential) - 微分制御
役割: 「偏差の未来の変化」を予測して動作します。目標位置に勢いよく近づきすぎている場合、事前にブレーキをかけるように働き、動きを安定させます。
課題: オーバーシュートや到達後の振動を抑える効果がありますが、ゲインが強すぎると動きが鈍くなることもあります。
これらP・I・Dの3つの要素の強さ(ゲイン)をうまく調整することで、ロボットアームを「速く、かつ滑らかに、そして正確に」目標位置へ動かすことが可能になります。
現状と解決方針
現在のrecord.pyでは、PID係数がP=16, I=0, D=32に設定されています。Iゲインが0であるため、これは実質的なPD制御として動作しています。

この設定が、特に重力下で以下のような問題を引き起こしています。
- 定常偏差: アームが水平に伸びた状態で、肩を支えるモーター2に「少しだけ上げる」指示を出しても、アームの重さに負けて目標位置に到達できない定常偏差が発生します。
- オーバーシュート: 逆に、アームを下げる際には重力で加速し、目標を通り過ぎるオーバーシュートが発生しやすくなります。
- Punch値の課題: モーターの初期トルクを決める
Punch値が、CW(時計回り)とCCW(反時計回り)で共通のため、重力に逆らう時と従う時で最適な設定ができず、妥協が必要でした。
自動的にPIDゲインを変えながら動きを記録するコードを作成して、モーターごとの負荷に応じて最良と思われる結果を示しました。
- PID調整の方針
- 調整法などもありますがCWとCCWで状況が違うこともあるためパラメータを振って確認しました
- PI制御
- P制御だけだと、定常偏差がのこる(目標値にならない) - Iを入れて目標到達させる - Iが入るとオーバーシュートしやすくなるのでP制御の時よりPを小さく - ただし、今回のモーターでは、Iは積分されて最小動くのが遅い仕様になっている - Iに1など小さい値を入れると、4,5秒経ってもまだ微妙に動くので、Iを入れるなら大きめに入れるもしくは0とした方がいい - オーバーシュートしないなら大きめに入れてしまう - PD制御
- P制御だけだと、目標到達後に振動する - Dを入れてブレーキをかける、だからP制御の時よりPを大きく - PID制御
- 目標値到達と振動抑制が両方いる - Iを入れるとオーバーシュートしたり、振動しやすいのでDを入れる - P→I→Dと調整していくことにする
試験結果
現在位置からCW(Step0)→CCW(Step1)→CW(Step2)と動かして元の位置に戻る時の動きを200Hzでデータ取得しました。 ポジションはすべて中間位置に設定しています。
shoulder_pan_motor1
- IもDもなくて良さそうで、12と16の間で14にする

- IもDもなくて良さそうで、12と16の間で14にする
shoulder_lift_motor2
- Step0とStep2の下降時にオーバーシュートが大きく目標値に到達できていない、Step1の上昇時は振動している
- Iを大きくして、目標値到達させる方が良さそう。Dも大きくして振動を抑える。

elbow_flex_motor3
- 2と同様だがオーバーシュート量は少なく、振動も少ないため、PIで考える。

- 2と同様だがオーバーシュート量は少なく、振動も少ないため、PIで考える。
wrist_flex_motor4
- P制御でも振動していない

- P制御でも振動していない
wrist_roll_motor5
- P制御でも振動していない

- P制御でも振動していない
gripper_motor6
- P制御でも振動していない

- P制御でも振動していない
今回設定したPIDゲイン
| Motor | P | I | D | 制御方式 | 主な理由 |
|---|---|---|---|---|---|
| 1 (shoulder_pan) | 14 | 0 | 0 | P制御 | 重力影響がほぼない |
| 2 (shoulder_lift) | 32 | 18 | 32 | PID制御 | 最も重力影響が大きく、全要素が必要 |
| 3 (elbow_flex) | 24 | 6 | 0 | PI制御 | 重力影響はあるが、振動は少ないため |
| 4 (wrist_flex) | 20 | 0 | 0 | P制御 | 重力影響がほぼない |
| 5 (wrist_roll) | 20 | 0 | 0 | P制御 | 重力影響がほぼない |
| 6 (gripper) | 20 | 0 | 0 | P制御 | 重力影響がほぼない |
この設定をキープするためには、record.pyなどで上書きされる部分をマスクする必要があります。
動きが急になるので、SRAMのAccelerationとVelocity値の調整を合わせたほうがいいです。
今後やりたいこと
Sim2Realも触ってみたいと思っていますが、プラントモデルにこういったモーター制御や外乱を組み込むことは重要なので、もう少し検証はしていきたいと思っています。 個人で遊んでるだけなのでいつできるかわかりませんが、今後は以下の点について改善を進めたいと考えています。
- EEPROMへの書き込み最適化: 毎回書き込むのではなく、現在の値を読み出し、変更がある場合のみ書き込むロジックを実装します。
- 加減速プロファイルの活用: SRAMの
AccelerationとVelocity値を調整し、急加速・急停止を防ぐ台形制御を導入して、より滑らかな動作を目指す。現在の設定では目標速度に達する前に減速が始まってしまい、速度プロファイルが三角形状になっています。これにより、動作の開始時と停止時に急な躍度(ジャーク)が発生し、振動や『カクつく』動きの原因になる可能性があります。 - 直接PWM制御の検討: サーボ内部のコントローラーをバイパスし、PC側でフィードバックループを構成したり、重力補償や把持物体に応じた制御を検討する。ネックになるのはUSBの通信部分で100Hz程度の制御でどうにかなるかを検証する必要があります。せっかく安い構成なのでそれをできるだけ活かしていきたいです。
最終的にはこのような調整をしなくても、ロボット基盤モデルが特性を考慮してよしなに動かしてくれるようになってくるのか、パラメーターを変えてもロボット基盤モデルが勝手に対応していくれるのか、といったところは注視していこうと思います。現状のモーター制御のままでもうまく動かせるモデルが出てくるか?という意味では、改善は行わずに検証していくのも一つの方向性だと思っていますし、今すでに大量にあるデータセットとずれてくると思うので目的に応じて何をするかは考えていくべきだと思います。あくまでも今回紹介したのはすごく簡略化した構成ですし、目的やユースケースに合わせて検討してみるといいかもしれないという感じで見ていただけるとありがたいです。
We Are Hiring!
ABEJAは、テクノロジーの社会実装に取り組んでいます。 技術はもちろん、技術をどのようにして社会やビジネスに組み込んでいくかを考えるのが好きな方は、下記採用ページからエントリーください! (新卒の方やインターンシップのエントリーもお待ちしております!)
