这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 用28BYJ-48电机从零搭一台三自由度机械臂

共1条 1/1 1 跳转至页

用28BYJ-48电机从零搭一台三自由度机械臂

助工
2026-10-05 11:04:32     打赏

一直想做一个机械臂,做一些电机控制学习,于是就有了这台基于28BYJ-48的三自由度机械臂。一台最廉价的什么也做不了的机械臂。

1、28BYJ-48电机简介

28BYJ-48是入门级步进电机里最常见的一款,便宜、量大。我买的是12V规格,每个电机配一块ULN2003驱动板。

优点是精度够用,4096半步转360°,1步=0.0879°,足以让机械臂按"度"来定位。缺点是扭矩小、只能带动轻负载;轻微抖动(转子和负载的惯量所致),换向时齿轮间隙还会带来一小段空程,所以驱动代码必须做加减速。

1791169206750216.png

2、机械臂简介

机械臂来源于开源项目:https://makerworld.com.cn/zh/models/2150801-bu-jin-dian-ji-28byj-48ji-jie-bi

1791169234611533.jpg

这套机械臂采用串联结构,三个关节自上而下分别是:

关节名称运动方向作用
J1底座水平旋转带动整条手臂左右转,决定"朝向"
J2肩俯仰抬起或放下大臂
J3肘俯仰决定小臂与末端的伸出角度

图

这套机械臂的缺点也很明显:

①没有位置传感器,角度全靠步数推算。解决办法是上电前手动把手臂摆回中间位,定义为0°,三轴行程限制在±90°。

②转轴有缝隙,角度无法完全到位,会有±3°左右的偏差。

3、驱动方式

主控选择ESP32-P4,三轴占用12个GPIO。ESP32的GPIO输出电流只有几十毫安,带不动电机线圈,所以每轴4路信号接ULN2003的四个输入端,电机五线中的公共端接12V。

关节IN1IN2IN3IN4
J1GPIO20GPIO21GPIO22GPIO23
J2GPIO31GPIO32GPIO33GPIO34
J3GPIO49GPIO50GPIO51GPIO52

驱动用八拍(半步)模式:A→AB→B→BC→C→CD→D→DA循环,比四拍(整步)更平顺,步距角再细分一半。转子转一圈需要64个半步,乘上64级减速齿轮,输出轴一圈就是4096个半步。

三轴采用同步模式:以行程最长的轴定速,同起同停。速度上限按负载区分,J1带动整条手臂,上限400Hz;J2、J3负载轻,能跑到600Hz。

4、运行效果

把三个关节摆到中间位,运行三轴联动的参考代码stepper3.py:

from machine import Pin
import time

STEPS_PER_REV = 4096          # 半步/圈:90° = 1024 步
LIMIT = 90.0                  # 软限位(度)
DIR = (-1, 1, -1)             # 方向校正:J2 已实测;J1/J3 若反,单独改对应项
MAX_HZ = (400, 600, 600)      # 各轴速度上限:J1 带整臂负载,可再降
RAMP_STEPS = 96               # 加减速步数上限(自动缩到行程的 1/4)
RAMP_F_MIN = 0.25             # 斜坡最低速度系数(起步/收尾不低于 hz*此值)
HOLD_MS = 150                 # 到位后继续保持通电 ms(吸收停车振荡再断电;0=立即断)
MAX_DT_MS = 5                 # 单次 tick 最多补 5ms 的步数(600Hz 下≈3 步)。
                             # 【关键】脉冲必须摊开:若一次补 24 步,脉冲间隔只有
                             # 几微秒,线圈来不及建立磁场 → 转子只抖不转。
                             # 代价:主循环 <200Hz 时整体速度会打折(宁可慢,不许爆发)
DBG = True                    # 串口调试输出

SEQ = ((1,0,0,0),(1,1,0,0),(0,1,0,0),(0,1,1,0),
      (0,0,1,0),(0,0,1,1),(0,0,0,1),(1,0,0,1))

class Stepper:
   def __init__(self, p1, p2, p3, p4, name="", dir_sign=1, max_hz=600, ramp=True):
       self.pins = (Pin(p1, Pin.OUT), Pin(p2, Pin.OUT),
                    Pin(p3, Pin.OUT), Pin(p4, Pin.OUT))
       self.name = name
       self.dir_sign = dir_sign
       self.max_hz = max_hz
       self.ramp = ramp
       self.seq = 0          # 相位表索引 0-7
       self.pos = 0          # 当前位置(半步,页面角度约定)
       self.to_go = 0        # 剩余步数
       self.moved = 0        # 本次运动已走步数(加速段判据)
       self.acc_n = 0        # 本次运动的加速步数(0=不加速)
       self.dec_n = 0        # 本次运动的减速步数
       self.off_ts = 0       # 到位后延迟断电的时间戳(0=无待断电)
       self.sd = 1           # 相位方向(物理)
       self.pd = 1           # 位置计数方向(页面约定)
       self.hz = 400         # 步进速度(半步/秒)
       self.acc = 0.0        # 步数累加器(带小数,随时间累积)

   def off(self):
       self.pins[0].value(0); self.pins[1].value(0)
       self.pins[2].value(0); self.pins[3].value(0)

J1 = Stepper(20, 21, 22, 23, "J1", DIR[0], MAX_HZ[0])
J2 = Stepper(31, 32, 33, 34, "J2", DIR[1], MAX_HZ[1])
J3 = Stepper(49, 50, 51, 52, "J3", DIR[2], MAX_HZ[2])
_axes = (J1, J2, J3)

_last_ms = time.ticks_ms()
_ticks = 0                    # 有效 tick 次数(诊断)
_dt_sum = 0                   # 累计真实流逝时间 ms(诊断)
_dt_max = 0                   # 本段最长 tick 间隔 ms(诊断)

def tick():
   """主循环调用:按真实流逝时间补步。必须频繁调用(>=200Hz)脉冲才平滑。"""
   global _last_ms, _ticks, _dt_sum, _dt_max
   now = time.ticks_ms()
   dt = time.ticks_diff(now, _last_ms)
   if dt <= 0:
       return
   _last_ms = now
   _ticks += 1
   _dt_sum += dt
   if dt > _dt_max:
       _dt_max = dt
   if dt > MAX_DT_MS:
       dt = MAX_DT_MS            # 限幅:慢一点没关系,绝不爆发式跳步
   for a in _axes:
       if a.to_go > 0:
           f = 1.0
           if a.acc_n and a.moved < a.acc_n:
               f = (a.moved + 1.0) / a.acc_n      # 加速段:线性升速
           elif a.dec_n and a.to_go < a.dec_n:
               f = (a.to_go + 1.0) / a.dec_n      # 减速段:线性降速,收尾趋近最低速
           if f < RAMP_F_MIN:
               f = RAMP_F_MIN
           a.acc += a.hz * f * dt / 1000.0
           while a.acc >= 1.0 and a.to_go > 0:
               a.acc -= 1.0
               a.seq = (a.seq + a.sd) & 7
               s = SEQ[a.seq]
               a.pins[0].value(s[0]); a.pins[1].value(s[1])
               a.pins[2].value(s[2]); a.pins[3].value(s[3])
               a.pos += a.pd
               a.moved += 1
               a.to_go -= 1
               if a.to_go == 0:                   # 到位:先保持通电吸振,再断电
                   if HOLD_MS > 0:
                       a.off_ts = time.ticks_ms() + HOLD_MS
                   else:
                       a.off()
       elif a.off_ts and time.ticks_diff(time.ticks_ms(), a.off_ts) >= 0:
           a.off()
           a.off_ts = 0

def _clamp_deg(deg):
   return max(-LIMIT, min(LIMIT, deg))

def _deg_to_steps(deg):
   return int(round(deg * STEPS_PER_REV / 360.0))

def _target_axis(a, deg, hz):
   a.hz = max(20, min(a.max_hz, int(hz)))
   tgt = _deg_to_steps(_clamp_deg(deg))
   d = tgt - a.pos
   a.acc = 0.0
   a.moved = 0
   a.off_ts = 0
   if d == 0:
       a.to_go = 0
       return
   # 运动中同向改道:已在速度上,跳过加速段直接巡航(只留减速段)。
   # 否则跟踪时每次延长目标都从 25% 低速重新加速,看着一顿一顿"卡壳"。
   same_dir = a.to_go > 0 and a.pd == (1 if d > 0 else -1)
   a.to_go = abs(d)
   a.pd = 1 if d > 0 else -1
   a.sd = a.pd * a.dir_sign
   # 加减速步数:按行程 1/4 缩,短行程自动不做斜坡
   n = a.to_go // 4
   if n > RAMP_STEPS:
       n = RAMP_STEPS
   if n < 4:
       n = 0
   if not a.ramp:
       n = 0
   if same_dir and n:
       a.acc_n = 0
       a.dec_n = n
   else:
       a.acc_n = n
       a.dec_n = n

def set_targets(t1, t2, t3, hz=400, sync=False):
   """三轴绝对角度(页面约定,逆时针为正),越界自动夹到 ±90°。
   本函数只设目标立即返回,实际步进由主循环 tick() 执行。
   sync=True:三轴同起同停(最长行程轴按 hz 跑);
   sync=False:各轴都按 hz 跑,先到先停。"""
   ts = (t1, t2, t3)
   ds = []
   for i, a in enumerate(_axes):
       ds.append(abs(_deg_to_steps(_clamp_deg(ts[i])) - a.pos))
   m = max(ds)
   for i, a in enumerate(_axes):
       h = hz
       if sync and m > 0:
           h = int(hz * ds[i] / m)
           if ds[i] > 0 and h < 20:
               h = 20
       _target_axis(a, ts[i], h)
       if DBG:
           print("[st] %s 目标 %7.1f°  剩余 %4d 步  hz %3d  加减速 %d 步"
                 % (a.name, ts[i], a.to_go, a.hz, a.acc_n))

def move_joints(t1, t2, t3, hz=400, sync=False):
   set_targets(t1, t2, t3, hz, sync)

def home(hz=300):
   """三轴回 0°(上电中间位)"""
   set_targets(0.0, 0.0, 0.0, hz, True)

def stop_all():
   """急停:立即停在当前位置并断电"""
   for a in _axes:
       a.to_go = 0
       a.acc = 0.0
       a.off_ts = 0
       a.off()

def angles():
   return tuple(a.pos * 360.0 / STEPS_PER_REV for a in _axes)

def moving():
   return any(a.to_go > 0 for a in _axes)

def steps_left():
   """三轴剩余步数(诊断用)"""
   return tuple(a.to_go for a in _axes)

def stats(reset=False):
   """(有效 tick 次数, 累计真实流逝 ms, 本段最长间隔 ms) —— 诊断用:
   读心跳时应看到 tick 次数/秒 >= 200、最长间隔 <= 5ms 才算健康。"""
   global _dt_max
   r = (_ticks, _dt_sum, _dt_max)
   if reset:
       _dt_max = 0
   return r

if __name__ == "__main__":
   print("自检:三轴同步 +45° 再回 0(先确保上电位即 0°)")
   move_joints(45, 45, 45, 300, True)
   while moving():
       tick()
       time.sleep_ms(2)
   print("到位:", ["%.1f" % x for x in angles()])
   home(300)
   while moving():
       tick()
       time.sleep_ms(2)
   print("已回中:", ["%.1f" % x for x in angles()])
   print("tick 统计:", stats())

运行后三轴会一起转到45°,再回到原点。

这台机械臂做电机控制学习够用,干不了精细活。





关键词: 机械臂    

共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]