程序员花了1个月时间,手搓低成本机械臂:跟着他你也能复刻一台
off999 2025-07-19 22:01 126 浏览 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,跟着视频做,大体没有问题,但也确实碰到了不少小问题,包括视频里面没有提到的或者不清晰的,这里做个记录,有几个点:
- 舵机能安装线的可以提前安装好,卡住了再装会发现不好弄
- 舵机提前标记好,我用铅笔写的就还好,贴纸太丑
- 飞特给的螺丝足够的,不用怕少了,最后剩了一堆,给好评
- 套筒那个打印件有方向的,卡不进去不一定时打印误差,可能就是反了,调个头就好
- 舵机控制板的安装螺丝没的,我直接把板子带的铜柱敲进安装板上了,或者你用打火机烧一下,推进去就可以
- 夹爪那部分一定要看仔细视频,就是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 ·
相关推荐
- 安全教育登录入口平台(安全教育登录入口平台官网)
-
122交通安全教育怎么登录:122交通网的注册方法是首先登录网址http://www.122.cn/,接着打开网页后,点击右上角的“个人登录”;其次进入邮箱注册,然后进入到注册页面,输入相关信息即可完...
- 大鱼吃小鱼经典版(大鱼吃小鱼经典版(经典版)官方版)
-
大鱼吃小鱼小鱼吃虾是于谦跟郭麒麟的《我的棒儿呢?》郭德纲说于思洋郭麒麟作诗的相声,最后郭麒麟做了一首,师傅躺在师母身上大鱼吃小鱼小鱼吃虾虾吃水水落石出师傅压师娘师娘压床床压地地动山摇。...
-
- 哪个软件可以免费pdf转ppt(免费的pdf转ppt软件哪个好)
-
要想将ppt免费转换为pdf的话,我们建议大家可以下一个那个wps,如果你是会员的话,可以注册为会员,这样的话,在wps里面的话,就可以免费将ppt呢转换为pdfpdf之后呢,我们就可以直接使用,不需要去直接不需要去另外保存,为什么格式转...
-
2026-02-04 09:03 off999
- 电信宽带测速官网入口(电信宽带测速官网入口app)
-
这个网站看看http://www.swok.cn/pcindex.jsp1.登录中国电信网上营业厅,宽带光纤,贴心服务,宽带测速2.下载第三方软件,如360等。进行在线测速进行宽带测速时,尽...
- 植物大战僵尸95版手机下载(植物大战僵尸95 版下载)
-
1可以在应用商店或者游戏平台上下载植物大战僵尸95版手机游戏。2下载教程:打开应用商店或者游戏平台,搜索“植物大战僵尸95版”,找到游戏后点击下载按钮,等待下载完成即可安装并开始游戏。3注意:确...
- 免费下载ppt成品的网站(ppt成品免费下载的网站有哪些)
-
1、Chuangkit(chuangkit.com)直达地址:chuangkit.com2、Woodo幻灯片(woodo.cn)直达链接:woodo.cn3、OfficePlus(officeplu...
- 2025世界杯赛程表(2025世界杯在哪个国家)
-
2022年卡塔尔世界杯赛程公布,全部比赛在卡塔尔境内8座球场举行,2022年,决赛阶段球队全部确定。揭幕战于当地时间11月20日19时进行,由东道主卡塔尔对阵厄瓜多尔,决赛于当地时间12月18日...
- 下载搜狐视频电视剧(搜狐电视剧下载安装)
-
搜狐视频APP下载好的视频想要导出到手机相册里方法如下1、打开手机搜狐视频软件,进入搜狐视频后我们点击右上角的“查找”,找到自已喜欢的视频。2、在“浏览器页面搜索”窗口中,输入要下载的视频的名称,然后...
- 永久免费听歌网站(丫丫音乐网)
-
可以到《我爱音乐网》《好听音乐网》《一听音乐网》《YYMP3音乐网》还可以到《九天音乐网》永久免费听歌软件有酷狗音乐和天猫精灵,以前要跳舞经常要下载舞曲,我从QQ上找不到舞曲下载就从酷狗音乐上找,大多...
- 音乐格式转换mp3软件(音乐格式转换器免费版)
-
有两种方法:方法一在手机上操作:1、进入手机中的文件管理。2、在其中选择“音乐”,将显示出手机中的全部音乐。3、点击“全选”,选中所有音乐文件。4、点击屏幕右下方的省略号图标,在弹出菜单中选择“...
- 电子书txt下载(免费的最全的小说阅读器)
-
1.Z-library里面收录了近千万本电子书籍,需求量大。2.苦瓜书盘没有广告,不需要账号注册,使用起来非常简单,直接搜索预览下载即可。3.鸠摩搜书整体风格简洁清晰,书籍资源丰富。4.亚马逊图书书籍...
- 最好免费观看高清电影(播放免费的最好看的电影)
-
在目前的网上选择中,IMDb(互联网电影数据库)被认为是最全的电影网站之一。这个网站提供了各种类型的电影和电视节目的海量信息,包括剧情介绍、演员表、评价、评论等。其还提供了有关电影制作背后的详细信息,...
- 孤单枪手2简体中文版(孤单枪手2简体中文版官方下载)
-
要将《孤胆枪手2》游戏的征兵秘籍切换为中文,您可以按照以下步骤进行操作:首先,打开游戏设置选项,通常可以在游戏主菜单或游戏内部找到。然后,寻找语言选项或界面选项,点击进入。在语言选项中,选择中文作为游...
欢迎 你 发表评论:
- 一周热门
-
-
抖音上好看的小姐姐,Python给你都下载了
-
全网最简单易懂!495页Python漫画教程,高清PDF版免费下载
-
飞牛NAS部署TVGate Docker项目,实现内网一键转发、代理、jx
-
win7系统还原步骤图解(win7还原电脑系统的步骤)
-
Python 3.14 的 UUIDv6/v7/v8 上新,别再用 uuid4 () 啦!
-
python入门到脱坑 输入与输出—str()函数
-
16949认证费用是多少(16949审核员太难考了)
-
linux软件(linux软件图标)
-
Python三目运算基础与进阶_python三目运算符判断三个变量
-
windows7旗舰版多少钱(win7旗舰版要多少钱)
-
- 最近发表
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python进度条 (67)
- python吧 (67)
- python的for循环 (65)
- python格式化字符串 (61)
- python静态方法 (57)
- python列表切片 (59)
- python面向对象编程 (60)
- python 代码加密 (65)
- python串口编程 (77)
- python封装 (57)
- python写入txt (66)
- python读取文件夹下所有文件 (59)
- python操作mysql数据库 (66)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python多态 (60)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)
