百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

程序员花了1个月时间,手搓低成本机械臂:跟着他你也能复刻一台

off999 2025-07-19 22:01 5 浏览 0 评论

在开源硬件的广阔天地中,SO-ARM100 作为一款备受瞩目的开源机械臂项目脱颖而出。它以标准化的四轴机械臂设计为核心,构建起一个开放共享的技术平台,为机械臂爱好者与开发者提供了绝佳的远程操作实践场域和编程学习范本。

无论是想探索机械臂控制原理,还是钻研自动化编程逻辑,该项目都能满足多样化的学习需求。目前,项目代码、精密的设计文件以及详尽的使用手册,均已完整开源在 GitHub 平台开源。 今天,就让我们跟随 EEWorld 论坛资深网友 LitchiCheng 的脚步,一同踏入低成本复刻机械臂 SO-ARM100 的奇妙旅程,揭开这款开源机械臂从代码蓝图到实体运作的神秘面纱 。


3D 打印篇



原文链接:
https://www.eeworld.com.cn/am10mf5

清理了下许久不用的3D打印机,挤出机也裂了,更换了喷嘴和挤出机夹具,终于恢复了正常工作的状态,接下来还是要用起来,不然吃灰生锈了,于是乎想起了之前仿真中的SO-ARM100这个机械臂,打算做一套玩玩。
https://github.com/TheRobotStudio/SO-ARM100/tree/main#

SO-ARM100 机械臂有 6 个自由度,支持 3D 打印,性价比超高,是 Lerobot 开源机器人解决方案的一部分。

git clone后3d打印文件这里我用这两个stl文件,另外几个太大,我这个型号一盘放不下。

以前买的天鹰座,现在不知道出多少代了,好像最近比较火的是拓竹,以后再看要不要更新。

Part1摆放好后还可以。

Part2也刚刚好。

然后就开始打印了。


材料齐活篇



原文链接:
https://www.eeworld.com.cn/aibPS8S

打印件基本ok,总共12个,尴尬的是github又更新了so-101,不过看了下还好只是优化了走线和几个结构键,影响不大,大不了后面再重新打印(有3d打印机,哈哈哈)

舵机买的飞特的12V的版本,因为手上有12V的电源,不想在整个5V的电源,所以买了12V,比7.4V贵10 rmb差不多。

舵机控制板就是微雪家的。

材料齐活,准备开干。


舵机配置篇(WSL)



原文链接
https://www.eeworld.com.cn/aqnLW5S

飞特舵机:


组装之前需要配置舵机的ID,如下的网址为舵机的资料,实际上用不到,但可以mark在这里
https://www.feetechrc.com/software.html

User Guide里面可以下载操作手册

通信协议,嵌入式狂喜~

微雪舵机控制板:


控制板资料如下,默认就是USB的方式,跳线帽在B位置
www.waveshare.net/wiki/Bus_Servo_Adapter_(A)_

配置:


如下图,插上舵机线,注意通道的顺序,通上电源后。

Windows下会有usb serial的设备,因为我用的WSL,所以需要把这个usb attach给WSL中。

使用 ls /dev 看到出现了 ttyAMC0

执行加权指令,为了给python运行时的访问,不然有权限问题。

一个一个配置,总共6个舵机。


组装篇(打螺丝喽)



原文链接:
https://www.eeworld.com.cn/aD4iL0C

组装的视频有很多,参考大佬的《手把手复刻HuggingFace开源神作之Follower机械臂组装,资料已整理》_哔哩哔哩_bilibili,跟着视频做,大体没有问题,但也确实碰到了不少小问题,包括视频里面没有提到的或者不清晰的,这里做个记录,有几个点:

  1. 舵机能安装线的可以提前安装好,卡住了再装会发现不好弄
  2. 舵机提前标记好,我用铅笔写的就还好,贴纸太丑
  3. 飞特给的螺丝足够的,不用怕少了,最后剩了一堆,给好评
  4. 套筒那个打印件有方向的,卡不进去不一定时打印误差,可能就是反了,调个头就好
  5. 舵机控制板的安装螺丝没的,我直接把板子带的铜柱敲进安装板上了,或者你用打火机烧一下,推进去就可以
  6. 夹爪那部分一定要看仔细视频,就是6号舵机,我卡错方向,拔了好久拔不出来


标定篇



原文链接:
https://www.eeworld.com.cn/auXvrrP

组装完机械臂后,要进行初始标定,参考github的markdown
lerobot/examples/10_use_so100.md at main · huggingface/lerobot

只有从臂,所以arms里面只填follower即可

python lerobot/scripts/control_robot.py \
  --robot.type=so100 \
  --robot.cameras='{}' \
  --control.type=calibrate \
  --control.arms='["main_follower"]'

直接运行一般会报错,因为configs.py中配置的串口是2和3,需要改成机器中的,比如ttyACM0,然后参考控制台输出的内容,按照图示将这些位置依次摆好,然后回车。

最后标定完之后,会将标定数据保存在
.cache/calibration/so100/main_follower.json

打开这个文件看下。


上位机控制调试



原文链接:
https://www.eeworld.com.cn/aOqffjT

SO-ARM100机械臂组装并且标定完成后,下一步就是整臂的调试,由于只做了follower这个从臂,所以要使用lerobot仓库中遥操作控制的方式就不行了,这里发现了bambot这个开源仓库,感谢大佬,通过web的形式可以可视化控制so-arm100这个机械臂,没有其他的依赖,赶紧点个星!
https://bambot.org/play/so-arm100

可以在windows或其他有浏览器访问的地方均可,只需要调用串口设备。

在启动的时候选择对应串口就可以连接。

这里应该是增量式的控制,注意上电控制前的位置要和这个页面中的保持一致,拖动joint的控制即可。


单关节控制(附代码)



原文链接:
https://www.eeworld.com.cn/ab1uPq1

代码仓库:GitHub - LitchiCheng/SO-ARM100: Some Test code on SO-ARM100

用bambot的web的方式调试了整个机械臂,对于后面的仿真的sim2real来说,还是需要单独封装好这些控制,方便后面迁移到其他的测试平台中。

翻了lerobot的代码,可以看到对于feetech舵机的控制等封装已经挺完善,本质就是通过串口和舵机进行协议通信,这个在
lerobot/lerobot/common/robot_devices/motors/feetech.py
中可以看下相应的寄存器定义。

接下来我们就针对lerobot这部分代码进行测试,测试单个关节的运动,首先是创建一个FeetechMotor的类,传入port和id就就可以方便的调试某个电机。

import sys
import os
import time
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(parent_dir)


from feetech import FeetechMotorsBusConfig
from feetech import FeetechMotorsBus


class FeetechMotor:
    def __init__(self, motor_id, port="/dev/ttyACM0"):
        motors={
            # name: (index, model)
            "shoulder_pan": [1, "sts3215"],
            "shoulder_lift": [2, "sts3215"],
            "elbow_flex": [3, "sts3215"],
            "wrist_flex": [4, "sts3215"],
            "wrist_roll": [5, "sts3215"],
            "gripper": [6, "sts3215"],
        },
        self.motor_id = motor_id
        self.motors_bus = FeetechMotorsBus(FeetechMotorsBusConfig(
            port=port,
            motors=motors,
        ))
        self.motors_bus.connect()


    def setPosition(self, position):
        self.motors_bus.write_with_motor_ids(self.motors_bus.motor_models, self.motor_id, "Goal_Position", position)


    def getPosition(self):
        return self.motors_bus.read_with_motor_ids(self.motors_bus.motor_models, self.motor_id, "Present_Position")


    def close(self):
        self.motors_bus.disconnect()

实验测试目标,让某个关节往复运动,声明一个函数generatePositionSequence用来生成位置的序列。

def generatePositionSequence(start_position, range_value, loops=1):
    sequence = []
    for _ in range(loops):
        forward_positions = list(range(start_position, start_position + range_value + 1))
        sequence.extend(forward_positions)
        backward_positions = list(range(start_position + range_value - 1, start_position - 1, -1))
        sequence.extend(backward_positions)
    return sequence

执行部分,指定电机id和串口名称,开始让电机运动到2048也就是中间的位置,然后再开始往复运动。

if __name__ == "__main__":
    motor = FeetechMotor(5, "/dev/ttyACM0")
    motor.setPosition(2048)
    time.sleep(1)
    start_position = motor.getPosition()
    print(f"Start position: {start_position}")
    range_val = 600
    loop_count = 10
    result = generatePositionSequence(start_position, range_val, loop_count)
    for position in result:
        motor.setPosition(position)
        current_position = motor.getPosition()
        print(f"Current position: {current_position}, Goal position: {position}")
        time.sleep(0.005)
    motor.close()


从lerobot中剥离Feetech舵机控制代码



原文链接:
https://www.eeworld.com.cn/avbnbTC

代码仓库:GitHub - LitchiCheng/SO-ARM100: Some Test code on SO-ARM100

在上面《复刻低成本机械臂 SO-ARM100 单关节控制(附代码)》中,使用lerobot进行了SO-ARM100关节的控制测试,对于在lerobot场景下是可以满足使用,但需要安装lerobot的较多依赖。

下面,将SO-ARM100中的Feetech舵机控制代码从lerobot中进行剥离,仅保留feetech本身的sdk依赖。在后面其他实验的测试就可以独立来,不依赖于lerobot的环境,如ROS2的实机控制等。

feetech sdk的python package为
Adam-Software/Feetech-Servo-SDK: Feetech Servo Python SDK. Copy official repository.仓库

安装方式为:

pip3 install feeteck-servo-sdk

其中有sdk和examples


导入包的方式为:

import scservo_sdk

参考lerobot的宏定义及函数后,剥离后的独立封装代码如下:

import time
import macro
import scservo_sdk as scs


def convert_to_bytes(value, bytes):
    if bytes == 1:
        data = [
            scs.SCS_LOBYTE(scs.SCS_LOWORD(value)),
        ]
    elif bytes == 2:
        data = [
            scs.SCS_LOBYTE(scs.SCS_LOWORD(value)),
            scs.SCS_HIBYTE(scs.SCS_LOWORD(value)),
        ]
    elif bytes == 4:
        data = [
            scs.SCS_LOBYTE(scs.SCS_LOWORD(value)),
            scs.SCS_HIBYTE(scs.SCS_LOWORD(value)),
            scs.SCS_LOBYTE(scs.SCS_HIWORD(value)),
            scs.SCS_HIBYTE(scs.SCS_HIWORD(value)),
        ]
    else:
        raise NotImplementedError(
            f"Value of the number of bytes to be sent is expected to be in [1, 2, 4], but "
            f"{bytes} is provided instead."
        )
    return data


class FeetechMotor:
    def __init__(self, motor_id, port="/dev/ttyACM0"):
        self.motor_id = motor_id
        self.port = port


    def connect(self):
        self.port_handler = scs.PortHandler(self.port)
        self.packet_handler = scs.PacketHandler(macro.PROTOCOL_VERSION)
        try:
            if not self.port_handler.openPort():
                raise OSError(f"Failed to open port '{self.port}'.")
        except Exception:
            print("choose right port!")
            raise


        self.is_connected = True
        self.port_handler.setPacketTimeoutMillis(macro.TIMEOUT_MS)


    def disconnect(self):
        if self.port_handler is not None:
            self.port_handler.closePort()
            self.port_handler = None


        self.packet_handler = None
        self.is_connected = False


    def set_bus_baudrate(self, baudrate):
        present_bus_baudrate = self.port_handler.getBaudRate()
        if present_bus_baudrate != baudrate:
            print(f"Setting bus baud rate to {baudrate}. Previously {present_bus_baudrate}.")
            self.port_handler.setBaudRate(baudrate)


            if self.port_handler.getBaudRate() != baudrate:
                raise OSError("Failed to write bus baud rate.")


    def read_with_motor_ids(self, motor_ids, data_name, num_retry=macro.NUM_READ_RETRY):
        return_list = True
        if not isinstance(motor_ids, list):
            return_list = False
            motor_ids = [motor_ids]


        addr, bytes = macro.SCS_SERIES_CONTROL_TABLE[data_name]
        group = scs.GroupSyncRead(self.port_handler, self.packet_handler, addr, bytes)
        for idx in motor_ids:
            group.addParam(idx)


        for _ in range(num_retry):
            comm = group.txRxPacket()
            if comm == scs.COMM_SUCCESS:
                break


        if comm != scs.COMM_SUCCESS:
            raise ConnectionError(
                f"Read failed due to communication error on port {self.port_handler.port_name} for indices {motor_ids}: "
                f"{self.packet_handler.getTxRxResult(comm)}"
            )


        values = []
        for idx in motor_ids:
            value = group.getData(idx, addr, bytes)
            values.append(value)


        if return_list:
            return values
        else:
            return values[0]


    def write_with_motor_ids(self, motor_ids, data_name, values, num_retry=macro.NUM_WRITE_RETRY):
        if not isinstance(motor_ids, list):
            motor_ids = [motor_ids]
        if not isinstance(values, list):
            values = [values]


        addr, bytes = macro.SCS_SERIES_CONTROL_TABLE[data_name]
        group = scs.GroupSyncWrite(self.port_handler, self.packet_handler, addr, bytes)
        for idx, value in zip(motor_ids, values, strict=True):
            data = convert_to_bytes(value, bytes)
            group.addParam(idx, data)


        for _ in range(num_retry):
            comm = group.txPacket()
            if comm == scs.COMM_SUCCESS:
                break


        if comm != scs.COMM_SUCCESS:
            raise ConnectionError(
                f"Write failed due to communication error on port {self.port_handler.port_name} for indices {motor_ids}: "
                f"{self.packet_handler.getTxRxResult(comm)}"
            )


    def setPosition(self, position):
        self.write_with_motor_ids(self.motor_id, "Goal_Position", position)


    def getPosition(self):
        return self.read_with_motor_ids(self.motor_id, "Present_Position")


def generatePositionSequence(start_position, range_value, loops=1):
    sequence = []
    for _ in range(loops):
        forward_positions = list(range(start_position, start_position + range_value + 1))
        sequence.extend(forward_positions)
        backward_positions = list(range(start_position + range_value - 1, start_position - 1, -1))
        sequence.extend(backward_positions)
    return sequence


if __name__ == "__main__":
    motor = FeetechMotor(6, "/dev/ttyACM0")
    motor.connect()
    motor.setPosition(2048)
    time.sleep(1)
    start_position = motor.getPosition()
    print(f"Start position: {start_position}")
    range_val = 600
    loop_count = 10
    result = generatePositionSequence(start_position, range_val, loop_count)
    for position in result:
        motor.setPosition(position)
        current_position = motor.getPosition()
        print(f"Current position: {current_position}, Goal position: {position}")
        time.sleep(0.005)
    motor.disconnect()

clone仓库后,在确认串口为/dev/ttyACM0后,可直接python3 FeetechMotor.py进行测试。

· END ·

相关推荐

Modbus RTU 指令基本功能介绍(modbus-rtu)

ModbusRTU协议概述:入门级知识点ModbusRTU协议,是工业自动化领域应用广泛的串行通信协议。它简单、可靠,在各种工业设备之间建立通信桥梁,实现数据的采集和控制。ModbusRTU...

AIOT开发选型:行空板 K10 与 M10 适用场景与选型深度解析

前言随着人工智能和物联网技术的飞速发展,越来越多的开发者、学生和爱好者投身于创意项目的构建。在众多的开发板中,行空板K10和M10以其独特的优势脱颖而出。本文旨在为读者提供一份详尽的行空板K...

程序员花了1个月时间,手搓低成本机械臂:跟着他你也能复刻一台

在开源硬件的广阔天地中,SO-ARM100作为一款备受瞩目的开源机械臂项目脱颖而出。它以标准化的四轴机械臂设计为核心,构建起一个开放共享的技术平台,为机械臂爱好者与开发者提供了绝佳的远程操作实践场域...

RPC接口测试技术-Tcp 协议的接口测试

首先明确Tcp的概念,针对Tcp协议进行接口测试,是指基于Tcp协议的上层协议比如Http,串口,网口,Socket等。这些协议与Http测试方法类似(具体查看接口自动化测试章...

同事开玩笑说:你这个python程序要是外流出去了,可能会有危险

引言公司因为业务原因,购入了一些高灵敏高精度的振动传感器。老板说:“拿去进行测试,看看数据如何?”吭哧吭哧接入数据,一看,确实精度和灵敏度非常高。具体多高呢?将传感器固定在相关的结构物上,在办公室中人...

STM32搭建简易环境监测站并通过网络实时上报

一、系统总体架构本系统以STM32F407为核心,搭建一个环境监测节点,能够采集温湿度、光照、空气质量等数据,并通过OLED屏显示,同时通过ESP8266模块实现局域网数据上报。适合室内空气监测、智慧...

STM32通过NB-IoT模块实现远程告警推送

一、项目概述本系统以STM32F103C8T6作为主控核心,通过串口控制NB-IoT通信模块(移远BC26),实现对外设状态的远程监测和异常自动告警推送(如温度超限、设备震动异常等)。支持通过UDP或...

MicroPython 玩转硬件系列3:上电执行程序

1.引言上一篇:MicroPython玩转硬件系列2:点灯实验我们在ESP32上实现了LED灯的闪烁,但是有一个问题,该功能的实现需要我们在串口终端里去手动执行代码,可不可以让ESP32上电后自动...

打标机与上位机通讯异常如何快速定位?串口工具验证流程拆解

打标机与上位机通信过程中出现的错误问题需要通过串口通信助手验证,主要原因在于串口通信的底层特性以及问题隔离的工程需求。以下是原理说明和验证方法:一、验证原理底层数据透明化串口通信本质上是基于二进制数据...

4G短信猫发送中文短信(Python)(4g短信)

4G短信猫发送中文短信(Python)4G短信猫发送中文短信的方式可以使用TEXT模式或者PDU模式。1.TEXT模式在TEXT模式下发送中文短信的指令序列:AT+CSCS="UCS2...

ESP32如何刷microPython固件(esp32 固件升级)

目录为什么要刷microPython固件固件和工具的获取刷固件的步骤检验是否成功1.为什么要刷固件microPython是由计算机工程师DamienGeorge设计出来的,他的初衷是——用Pyth...

CH9329双头线使用说明(双头线是干什么用的)

目录1.介绍说明2.测试说明3.修改为ASCII模式(CH9328字符模式)常见问题解答:1.介绍说明CH9329双头线是集成了CH9329+CH340芯片的成品线,主要作用是使用主控电脑发送串口指令...

Windows下最简单的ESP8266_ROTS_ESP-IDF环境搭建与腾讯云SDK编译

前言其实也没啥可说的,只是我感觉ESP-IDF对新手来说很不友好,很容易踩坑,尤其是对业余DIY爱好者搭建环境非常困难,即使有官方文档,或者网上的其他文档,但是还是很容易踩坑,多研究,记住两点就行了,...

CPU眼里的:Python 和 C(cpp和python)

“Python跟C语言有什么联系?它们在计算机系统中分别扮演着什么角色?”01提出问题Python可能是当今最热门的编程语言,凭借简洁易读的语法和强大的生态,成为许多新手程序员的首选。然而,作为一门解...

Python在工控领域的应用与优势(python工业控制系统)

前言之前利用Python编写了一些S7系列的PLC调试工具和组态开发,今天就具体讲讲Python在工控领域还有哪些应用与优势。Python在工业控制工控领域的应用逐渐增多,得益于其简洁的语法、丰富的生...

取消回复欢迎 发表评论: