🤖

Feetech製STS3215シリアルサーボをPythonで動かしてみる

に公開

関連リンク

https://zenn.dev/usagi1975/articles/2026-05-16-000_sts3215-spec
https://zenn.dev/usagi1975/articles/2026-05-18-000_sts3215-python2

LeRobotフレームワークを使う前に,
STS3215 シリアルサーボをfeetech-servo-sdkを使って操作してみる.
LeRobotでも内部ではこのSDKを使って制御しているので,内部理解にもなるはず.

必要なもの

  • 5-7.4V, 4A程度のアダプタ (最終的に6個ぐらい接続するので4A以上あるといい)
  • Bus Servo アダプタ (シリアル通信と電源を束ねてサーボに送信)
  • STS3215
  • USBケーブル

接続のブロック図は以下.サーボ間は付属のケーブルで数珠つなぎする.

feetech-servo-sdk の導入

Ubuntu24.04 LTSで実施.venv環境内でセットアップしてます.

apt install python3.12-venv -y
python3 -m venv venv
source venv/bin/activate

pip install feetech-servo-sdk

USBのポート確認

シリアルサーボなので,モーターとのやり取りはシリアル通信で行うことができる.
まずモーターと通信するためには,デバイスパスボーレートを知っておく必要がある.

Bus Servo アダプタをPCに接続した直後に以下のコマンドで判別できる.

sudo dmesg | tail -n 20
:
[ 6341.571571] usb 1-8.2.4: New USB device found, idVendor=1a86, idProduct=55d3, bcdDevice= 4.45
[ 6341.571579] usb 1-8.2.4: New USB device strings: Mfr=0, Product=2, SerialNumber=3
[ 6341.571582] usb 1-8.2.4: Product: USB Single Serial
[ 6341.571586] usb 1-8.2.4: SerialNumber: 5B14111029
[ 6341.594163] cdc_acm 1-8.2.4:1.0: ttyACM0: USB ACM device

上記の場合,ttyACM0なので,デバイスパスは/dev/ttyACM0にだとわかる.
ボーレートはデフォルト 1000000 bpsになる.とりあえず変更することはなさそう.

モーターIDの取得と設定

モーターIDに関しては数珠つなぎされた中から特定のモーターに指示するために必ず必要になる.工場出荷状態では,どのモーターもID=1がEEPROMに保存されている.複数台同時に使う場合はこのIDが被らないように設定する必要がある.

まずはIDの取得だが,スマートには取得できない.以下のようにIDを1から順にping()関数を使って確認する.正しい反応が帰ってきたときのIDを返す関数を作成.

import sys
import scservo_sdk as scs
def get_motor_id(port="/dev/ttyACM0", baudrate=1000000):
    port_handler = scs.PortHandler(port)
    # STS / SMS series = 0, SCS series = 1
    packet_handler = scs.PacketHandler(0)
    port_handler.openPort()
    port_handler.setBaudRate(baudrate)

    # ping check
    motor_id = -1
    model_number = -1
    # 1 - 254
    for test_id in range(1, 254):
        found_model_number, comm, error = packet_handler.ping(port_handler, test_id)
        if comm == 0:
            motor_id = test_id
            model_number = found_model_number
            break
    port_handler.closePort()

    FEETECH_MODEL_NUMBER_TABLE = {
        "sts3215": 777,
        "sts3250": 2825
    }
    FEETECH_MODEL_PROTOCOL = {
        "sts3215": 0,
        "sts3250": 0
    }
    model_name = ""
    for k in FEETECH_MODEL_NUMBER_TABLE.keys():
        if FEETECH_MODEL_NUMBER_TABLE[k] == model_number:
            model_name = k
            break
    model_protocol = 0 # sts3215, sts3250
    return (motor_id, model_name, model_number, model_protocol)

(motor_id, model_name, model_number, model_protocol) = get_motor_id()
print("model_name=%s, new motor id=%d"%(model_name, motor_id))

接続したモーターのIDは3ということになる.

model_name=sts3215, new motor id=3

次にIDの設定だが,1−254までのIDを設定することができる.注意点としては,設定するときに既存のIDを指定する必要がある.なので,前述のID取得関数を使って現在のIDを取得して,それから新しいIDを設定する.

import sys
import scservo_sdk as scs
def set_motor_id(current_id, new_id, port="/dev/ttyACM0", baudrate=1000000):
    # init handler
    portHandler = scs.PortHandler(port)
    # STS / SMS series = 0, SCS series = 1
    packetHandler = scs.PacketHandler(0)

    # open port
    if not portHandler.openPort():
        print("failed to open port.")
        exit()

    # set baundrate
    if not portHandler.setBaudRate(baudrate):
        print("failed to set baudrate")
        exit()
    
    # EEPROM ID address
    ADDR_ID = 5 
    # write1ByteTxRx(port, motor_id, address, data)
    scs_comm_result, scs_error = packetHandler.write1ByteTxRx(portHandler, current_id, ADDR_ID, new_id)

    if scs_comm_result != scs.COMM_SUCCESS:
        print(f"comm error: {packetHandler.getTxRxResult(scs_comm_result)}")
    elif scs_error != 0:
        print(f"status error: {packetHandler.getRxPacketError(scs_error)}")
    else:
        print(f"succeeded to set motor id. %d => %d"%(current_id, new_id))
        
    # close port
    portHandler.closePort()
    return

(motor_id, model_name, model_number, model_protocol) = get_motor_id()
print("model_name=%s, new motor id=%d"%(model_name, motor_id))
set_motor_id(current_id=motor_id, new_id=1)

ID=1を設定

model_name=sts3215, new motor id=3
succeeded to set motor id. 3 => 1

各種情報取得

ついでにID以外のEEPROMの設定情報を取得する関数.

import sys
import scservo_sdk as scs

# Feetech特有の符号付き絶対値表現をPythonの整数に変換する
def decode_sign_magnitude(value, bit_size=16):
    sign_bit = 1 << (bit_size - 1)
    if value & sign_bit:
        return -(value & ~sign_bit)
    return value

# モーター情報取得
def get_motor_info(motor_id, model_name, port="/dev/ttyACM0", baudrate=1000000):

    # ── EEPROM領域(電源OFF後も保持)──────────────────────────────
    ADDR_BAUDRATE         = 6   # 通信速度インデックス
    ADDR_RETURN_DELAY     = 7   # 応答遅延時間 (x2 us, 0=最速)
    ADDR_MIN_LIMIT        = 9   # 最小位置リミット (2byte)
    ADDR_MAX_LIMIT        = 11  # 最大位置リミット (2byte)
    ADDR_MAX_TEMP_LIMIT   = 13  # 過熱保護しきい値 (°C)
    ADDR_MAX_VOLTAGE      = 14  # 最大電圧しきい値 (x10 V)
    ADDR_MIN_VOLTAGE      = 15  # 最小電圧しきい値 (x10 V)
    ADDR_MAX_TORQUE       = 16  # トルク上限 (2byte, 0~1000)
    ADDR_P_GAIN           = 21  # PIDのP係数
    ADDR_D_GAIN           = 22  # PIDのD係数
    ADDR_I_GAIN           = 23  # PIDのI係数
    ADDR_CW_DEAD_BAND     = 26  # CW方向の不感帯幅 (小さいと振動しやすい)
    ADDR_CCW_DEAD_BAND    = 27  # CCW方向の不感帯幅
    ADDR_HOMING_OFFSET    = 31  # ゼロ点オフセット (2byte, 符号付き)
    ADDR_OPERATING_MODE   = 33  # 動作モード (0=Servo, 1=Wheel)

    # ── RAM領域(電源OFF時リセット)────────────────────────────────
    ADDR_TORQUE_ENABLE    = 40  # トルクON/OFF (0=OFF, 1=ON)
    ADDR_ACCELERATION     = 41  # 加速度制限 (0=最大加速, 大きいほど緩やか)
    ADDR_GOAL_POSITION    = 42  # 目標位置 (2byte, 0~4095)
    ADDR_GOAL_TIME        = 44  # 移動時間指定 (2byte, ms単位, 0=Speed制御)
    ADDR_GOAL_SPEED       = 46  # 目標速度 (2byte, 0=最大速度)
    ADDR_LOCK             = 55  # EEPROMロック (0=書込可, 1=ロック)
    ADDR_PRESENT_POSITION = 56  # 現在位置 (2byte, 0~4095)
    ADDR_PRESENT_SPEED    = 58  # 現在速度 (2byte, 符号付き)
    ADDR_PRESENT_LOAD     = 60  # 現在負荷 (2byte, 符号付き)
    ADDR_PRESENT_VOLTAGE  = 62  # 現在電圧 (x10 V)
    ADDR_PRESENT_TEMP     = 63  # 現在温度 (°C)
    ADDR_MOVING           = 66  # 動作中フラグ (0=停止, 1=動作中)
    ADDR_PRESENT_CURRENT  = 69  # 現在電流 (2byte, mA)

    # ── Baudrateインデックス変換マップ ────────────────────────────
    BAUDRATE_MAP = {
        0: 1_000_000,
        1:   500_000,
        2:   250_000,
        3:   128_000,
        4:   115_200,
        5:    38_400,
        6:    19_200,
        7:     9_600,
    }

    # ── ポート初期化 ──────────────────────────────────────────────
    portHandler   = scs.PortHandler(port)
    packetHandler = scs.PacketHandler(0)  # 0=STS/SMS系, 1=SCS系

    if not portHandler.openPort():
        print(f"エラー: ポート {port} を開けませんでした。")
        sys.exit(1)
    if not portHandler.setBaudRate(baudrate):
        print(f"エラー: ボーレート {baudrate} を設定できませんでした。")
        portHandler.closePort()
        sys.exit(1)

    print(f"\n--- Motor ID: {motor_id} から情報を取得中 ---")

    # EEPROMアンロック(読み取りのみでも念のため)
    packetHandler.write1ByteTxRx(portHandler, motor_id, ADDR_LOCK, 0)

    def read1(addr):
        val, res, _ = packetHandler.read1ByteTxRx(portHandler, motor_id, addr)
        if res != scs.COMM_SUCCESS:
            print(f"  [WARN] addr={addr} 読み取り失敗: {packetHandler.getTxRxResult(res)}")
            return None
        return val

    def read2(addr):
        val, res, _ = packetHandler.read2ByteTxRx(portHandler, motor_id, addr)
        if res != scs.COMM_SUCCESS:
            print(f"  [WARN] addr={addr} 読み取り失敗: {packetHandler.getTxRxResult(res)}")
            return None
        return val

    # ── 読み取り ─────────────────────────────────────────────────
    baud_idx      = read1(ADDR_BAUDRATE)
    return_delay  = read1(ADDR_RETURN_DELAY)
    min_limit     = read2(ADDR_MIN_LIMIT)
    max_limit     = read2(ADDR_MAX_LIMIT)
    max_temp_lim  = read1(ADDR_MAX_TEMP_LIMIT)
    max_voltage   = read1(ADDR_MAX_VOLTAGE)
    min_voltage   = read1(ADDR_MIN_VOLTAGE)
    max_torque    = read2(ADDR_MAX_TORQUE)
    p_gain        = read1(ADDR_P_GAIN)
    d_gain        = read1(ADDR_D_GAIN)
    i_gain        = read1(ADDR_I_GAIN)
    cw_dead       = read1(ADDR_CW_DEAD_BAND)
    ccw_dead      = read1(ADDR_CCW_DEAD_BAND)
    homing_raw    = read2(ADDR_HOMING_OFFSET)
    op_mode       = read1(ADDR_OPERATING_MODE)
    torque_en     = read1(ADDR_TORQUE_ENABLE)
    acceleration  = read1(ADDR_ACCELERATION)
    goal_pos      = read2(ADDR_GOAL_POSITION)
    goal_time     = read2(ADDR_GOAL_TIME)
    goal_speed_raw= read2(ADDR_GOAL_SPEED)
    cur_pos       = read2(ADDR_PRESENT_POSITION)
    cur_speed_raw = read2(ADDR_PRESENT_SPEED)
    cur_load_raw  = read2(ADDR_PRESENT_LOAD)
    cur_voltage   = read1(ADDR_PRESENT_VOLTAGE)
    cur_temp      = read1(ADDR_PRESENT_TEMP)
    moving        = read1(ADDR_MOVING)
    cur_current   = read2(ADDR_PRESENT_CURRENT)

    portHandler.closePort()

    # ── デコード ─────────────────────────────────────────────────
    baudrate_bps  = BAUDRATE_MAP.get(baud_idx, f"unknown ({baud_idx})")
    homing_offset = decode_sign_magnitude(homing_raw, bit_size=16) if homing_raw is not None else None
    # Wheel Modeのみ速度が符号付き
    goal_speed    = decode_sign_magnitude(goal_speed_raw, bit_size=16) if op_mode == 1 else goal_speed_raw
    cur_speed     = decode_sign_magnitude(cur_speed_raw,  bit_size=16) if cur_speed_raw is not None else None
    cur_load      = decode_sign_magnitude(cur_load_raw,   bit_size=16) if cur_load_raw  is not None else None
    op_mode_str   = {0: "Servo Mode", 1: "Wheel Mode"}.get(op_mode, f"Unknown({op_mode})")

    # ── 表示 ─────────────────────────────────────────────────────
    print(f"Motor Information:")
    print(f"  ── 基本設定 (EEPROM) ──────────────────────────")
    print(f"  [Motor_ID]              : {motor_id}")
    print(f"  [Model_Name]            : {model_name}")
    print(f"  [Model_Resolution]      : 4096")
    print(f"  [Baudrate]              : {baudrate_bps} bps (idx={baud_idx})")
    print(f"  [Return_Delay_Time]     : {return_delay} (x2 us)")
    print(f"  [Operating_Mode]        : {op_mode} ({op_mode_str})")
    print(f"  [Min_Position_Limit]    : {min_limit}")
    print(f"  [Max_Position_Limit]    : {max_limit}")
    print(f"  [Homing_Offset]         : {homing_offset}")
    print(f"  ── 保護設定 (EEPROM) ──────────────────────────")
    print(f"  [Max_Temperature_Limit] : {max_temp_lim} °C")
    print(f"  [Max_Voltage]           : {max_voltage / 10:.1f} V" if max_voltage is not None else "  [Max_Voltage]           : N/A")
    print(f"  [Min_Voltage]           : {min_voltage / 10:.1f} V" if min_voltage is not None else "  [Min_Voltage]           : N/A")
    print(f"  [Max_Torque]            : {max_torque}")
    print(f"  ── PID・制御パラメータ (EEPROM) ────────────────")
    print(f"  [P_Gain]                : {p_gain}")
    print(f"  [D_Gain]                : {d_gain}")
    print(f"  [I_Gain]                : {i_gain}")
    print(f"  [CW_Dead_Band]          : {cw_dead}")
    print(f"  [CCW_Dead_Band]         : {ccw_dead}")
    print(f"  ── 動作設定 (RAM) ─────────────────────────────")
    print(f"  [Torque_Enable]         : {torque_en} ({'ON' if torque_en else 'OFF'})")
    print(f"  [Acceleration]          : {acceleration} (0=最大加速)")
    print(f"  [Goal_Time]             : {goal_time} ms (0=Speed制御)")
    print(f"  [Goal_Speed]            : {goal_speed} steps/sec (0=最大速度)")
    print(f"  ── 現在状態 (RAM) ─────────────────────────────")
    print(f"  [Goal_Position]         : {goal_pos}")
    print(f"  [Present_Position]      : {cur_pos}")
    print(f"  [Present_Speed]         : {cur_speed} steps/sec")
    print(f"  [Present_Load]          : {cur_load}")
    print(f"  [Present_Voltage]       : {cur_voltage / 10:.1f} V" if cur_voltage is not None else "  [Present_Voltage]       : N/A")
    print(f"  [Present_Temperature]   : {cur_temp} °C")
    print(f"  [Moving]                : {moving} ({'動作中' if moving else '停止中'})")
    print(f"  [Present_Current]       : {cur_current} mA")

(motor_id, model_name, model_number, model_protocol) = get_motor_id()
get_motor_info(motor_id, model_name)
--- Motor ID: 1 から情報を取得中 ---
Motor Information:
  ── 基本設定 (EEPROM) ──────────────────────────
  [Motor_ID]              : 1
  [Model_Name]            : sts3215
  [Model_Resolution]      : 4096
  [Baudrate]              : 1000000 bps (idx=0)
  [Return_Delay_Time]     : 0 (x2 us)
  [Operating_Mode]        : 0 (Servo Mode)
  [Min_Position_Limit]    : 0
  [Max_Position_Limit]    : 4095
  [Homing_Offset]         : 2047
  ── 保護設定 (EEPROM) ──────────────────────────
  [Max_Temperature_Limit] : 70 °C
  [Max_Voltage]           : 8.0 V
  [Min_Voltage]           : 4.0 V
  [Max_Torque]            : 1000
  ── PID・制御パラメータ (EEPROM) ────────────────
  [P_Gain]                : 32
  [D_Gain]                : 32
  [I_Gain]                : 0
  [CW_Dead_Band]          : 1
  [CCW_Dead_Band]         : 1
  ── 動作設定 (RAM) ─────────────────────────────
  [Torque_Enable]         : 0 (OFF)
  [Acceleration]          : 0 (0=最大加速)
  [Goal_Time]             : 0 ms (0=Speed制御)
  [Goal_Speed]            : 0 steps/sec (0=最大速度)
  ── 現在状態 (RAM) ─────────────────────────────
  [Goal_Position]         : 0
  [Present_Position]      : 1560
  [Present_Speed]         : 0 steps/sec
  [Present_Load]          : 0
  [Present_Voltage]       : 5.2 V
  [Present_Temperature]   : 31 °C
  [Moving]                : 0 (停止中)
  [Present_Current]       : 0 mA

次回は実際に回転させてみる.

Discussion