当前位置: 首页 > news >正文

基于 EtherCAT + CiA402 的双机械臂10°周期运动流程解析

完整链路可以理解成一句话:

命令行里的10°→ 程序读成amplitude=10.0→ 传给每个轴的运动模块 → 换算成编码器脉冲 → 每 1ms 写入 EtherCAT 的0x607A Target Position→ 伺服驱动器执行位置环 → 电机转动 → 机械臂关节真的运动。


1. 命令行输入 10°

比如你运行:

./dual_arm_aging --f0 m0.xml --f1 m1.xml --amplitude 10 --period 2

或者简写:

./dual_arm_aging --f0 m0.xml --f1 m1.xml -a 10 -p 2

这里的10会被程序读到:

double amplitude = 10.0, period = 2.0;

2.amplitude=10传给两个机械臂控制对象

主函数里创建两个 EtherCAT 控制任务:

ArmProgram master0(0); master0.init(..., amplitude, period); ArmProgram master1(1); master1.init(..., amplitude, period);

也就是说,命令行读到的10°被传进了:

ArmProgram::init(..., double amplitude_deg, double period_sec)

所以此时:

amplitude_deg = 10.0 period_sec = 2.0

M0 和 M1 两组机械臂都会收到这个运动参数。


3. EtherCAT 初始化,找到每个伺服轴

进入ArmProgram::init()后,程序会加载 ENI 文件:

task.load_eni(file_name, cycle_time);

然后在配置回调里扫描 EtherCAT 从站:

if (task.profile_no(sp) != 402) continue;

这句话的意思是:
程序只处理CiA402 伺服驱动器

然后给每个轴创建一个axis_data,里面记录:

ax->slave_pos = sp; ax->axis_id = axis_count++; ax->master_id = mid;

也就是说,每一个 EtherCAT 伺服轴都会被程序抽象成一个软件里的axis_data对象。


4. 注册 PDO:把 C++ 指针和伺服对象字典绑定

这一步非常关键。

程序会把几个 CiA402 对象注册到 PDO:

0x6040 control_word 0x6041 status_word 0x6060 mode_of_operation 0x6064 position_actual_value 0x607A target_position

代码里是这样:

task.try_register_pdo_entry(ax->control_word, sp, {0x6040 + off, 0}); task.try_register_pdo_entry(ax->mode_of_operation, sp, {0x6060 + off, 0}); task.try_register_pdo_entry(ax->target_position, sp, {0x607a + off, 0});

这一步完成之后,后面你在 C++ 里写:

*axis->target_position = 某个数;

就等价于在周期通信中给伺服驱动器写:

0x607A Target Position

这就是从软件变量到 EtherCAT PDO 的绑定关系。


5. 10°不是直接发给电机,要先换算成脉冲

伺服驱动器不认识“10°”这个概念,它真正接收的是编码器脉冲位置。

所以程序要先算:

10° = 多少个 position counts

你代码里每个轴都有:

double counts_per_deg() const { return static_cast<double>((1LL << motorBit) * gearRatio / 360.0); }

也就是:

每度脉冲数 = 2^motorBit × 减速比 / 360

普通轴:

motorBit = 24 gearRatio = 101 counts_per_deg = 2^24 × 101 / 360 ≈ 4,706,941 counts/deg

所以普通轴的 10°:

10° ≈ 47,069,411 counts

ti5 轴:

motorBit = 18 gearRatio = 1 counts_per_deg = 2^18 / 360 ≈ 728.18 counts/deg

所以 ti5 轴的 10°:

10° ≈ 7,281 counts

程序会根据从站位置判断普通轴还是 ti5 轴,然后设置不同的motorBitgearRatio


6. 把 10°参数放进每个轴的运动模块

初始化每个轴的时候,程序会执行:

prog.motion.set_params(amplitude_deg, period_sec, cycle_dt, ax->counts_per_deg());

这一步就把四个东西传给运动模块:

amplitude_deg = 10.0 // 角度幅值 period_sec = 2.0 // 周期 cycle_dt = 0.001 // 每周期时间,默认 1ms counts_per_deg = 每个轴自己的脉冲/度

所以到这里,运动模块已经知道:

这个轴要做 ±10° 正弦运动; 这个轴 1° 等于多少脉冲; 每 1ms 更新一次目标位置。

7. 伺服先上电使能,进入 CSP 模式

在真正运动之前,程序不是直接写目标位置,而是先让伺服进入可运行状态。

主函数会等待:

while (g_running && (!master0.is_all_enabled() || !master1.is_all_enabled()))

is_all_enabled()里判断的是:

(*ax->status_word & 0x006f) == 0x0027

也就是每个轴都进入:

operation enabled

只有进入这个状态,电机才真正可以响应目标位置。

同时在power::on_cycle()里,当状态机到switched_on时,程序会设置:

*axis->mode_of_operation = 8; // CSP

8就是CSP,周期同步位置模式
也就是说,你这个项目最终是靠周期性写目标位置控制机械臂。


8. 回零:先把关节拉到 0 附近

程序启动后会让你确认安全,然后执行:

master0.begin_homing(); master1.begin_homing();

在周期回调里,收到 homing 命令后:

for (auto &p : progs) p.motion.start_homing();

运动模块进入HOMING模式后,会逐步让cur_target靠近 0:

int32_t err = 0 - cur_target;

也就是说,程序先尝试让所有关节目标位置回到 0。
回零完成后,后面的 10° 周期运动才是围绕当前起始位置进行。


9. 按 Enter 开始 aging,记录每个轴的起始位置

回零完成后,你按 Enter:

master0.begin_aging(); master1.begin_aging();

在 EtherCAT 周期回调里,会执行:

for (auto &p : progs) p.motion.start_aging(*p.axis->position_actual_value);

这一步非常重要。

它会把每个轴当前实际位置记录为:

start_pos = actual_pos;

所以 10°周期运动不是绝对从 0 开始,而是:

从当前实际位置 start_pos 开始,做 ±10° 正弦摆动

如果回零后实际位置接近 0,那么就是围绕 0 做 ±10°。


10. 每 1ms 计算一次新的目标位置

真正的 10°运动发生在SineMotion::update()里。

代码是:

elapsed += cycle_dt; double amp_counts = amplitude_deg * counts_per_deg; int32_t offset = static_cast<int32_t>( amp_counts * std::sin(2.0 * M_PI * elapsed / period_sec)); cur_target = start_pos + offset;

也就是说:

目标位置 = 起始位置 + 10°对应脉冲数 × sin(2πt / period)

如果周期是 2 秒,那么关节目标角度就是:

t=0s 0° t=0.5s +10° t=1.0s 0° t=1.5s -10° t=2.0s 0°

所以这里的10°正弦运动的幅值,不是“一直转到 10°不动”。


11.program()把计算结果写到target_position

周期回调最后会执行:

for (auto &p : progs) p();

每个programoperator()里会先执行上电状态机:

power_.on_cycle();

如果伺服还没使能,就先保持当前位置:

*axis->target_position = *axis->position_actual_value;

如果已经使能成功,就执行:

int32_t actual = *axis->position_actual_value; *axis->target_position = motion.update(actual);

这句就是整个链路里最关键的一句:

*axis->target_position = motion.update(actual);

左边的target_position已经绑定到了 EtherCAT 的0x607A Target Position,右边的motion.update()根据命令行的10°算出了当前周期的目标脉冲。


12. EtherCAT 把0x607A发给伺服驱动器

因为前面已经完成了 PDO 映射:

axis->target_position 绑定到 0x607A

所以当程序写:

*axis->target_position = cur_target;

EtherCAT 主站在下一个通信周期会把这个值打包进 RxPDO,通过网线发给伺服驱动器。

这时数据流是:

C++变量 target_position → EtherCAT RxPDO → 0x607A Target Position → 伺服驱动器

伺服驱动器收到目标位置后,会由驱动器内部完成:

位置环 → 速度环 → 电流环 → 电机转动

上位机不直接控制电流,也不直接控制 PWM。你这层程序控制的是目标位置


13. 电机转了,机械臂关节才真的动

伺服驱动器执行目标位置后,电机会带动减速器和机械臂关节转动。

如果换算关系正确:

target_position 变化 47,069,411 counts ≈ 普通轴机械侧变化 10°

如果是 ti5 轴:

target_position 变化 7,281 counts ≈ ti5 轴变化 10°

所以真实运动链路是:

0x607A目标位置变化 → 伺服驱动器控制电机 → 电机轴转动 → 减速器输出 → 关节转动 → 机械臂运动

14. 实际位置反馈回来,程序显示角度

伺服驱动器还会把实际位置通过 TxPDO 返回给主站:

task.try_register_pdo_entry(ax->position_actual_value, sp, {0x6064 + off, 0});

也就是:

0x6064 Position Actual Value

显示线程里会调用:

m0->get_joint_angles(a0, 7); m1->get_joint_angles(a1, 7);

get_joint_angles()里面会做反向换算:

angles[i] = position_actual_value / counts_per_deg();

所以你屏幕上看到的M0: [...] M1: [...],就是把驱动器反馈的实际脉冲重新换算成角度后的结果。


整个链路画成一条线

命令行输入 --amplitude 10 ↓ boost 解析成 amplitude = 10.0 ↓ master0.init(..., amplitude, period) master1.init(..., amplitude, period) ↓ ArmProgram::init() ↓ 扫描 EtherCAT 从站,找到 CiA402 伺服轴 ↓ 注册 PDO: 0x6040 控制字 0x6041 状态字 0x6060 模式 0x6064 实际位置 0x607A 目标位置 ↓ 根据 motorBit 和 gearRatio 计算 counts_per_deg ↓ motion.set_params(10°, period, cycle_dt, counts_per_deg) ↓ CiA402 状态机上电 ↓ 设置 0x6060 = 8,也就是 CSP 模式 ↓ 按 Enter 开始 aging ↓ 记录当前实际位置 start_pos ↓ 每 1ms 执行一次: offset = 10° × counts_per_deg × sin(2πt / period) target = start_pos + offset ↓ 写入 *axis->target_position ↓ 等价于写入 EtherCAT 0x607A Target Position ↓ EtherCAT 周期帧发送给伺服驱动器 ↓ 伺服驱动器内部位置环控制电机 ↓ 电机通过减速器带动机械臂关节运动 ↓ 驱动器通过 0x6064 返回实际位置 ↓ 程序换算成角度并显示

最核心的三句话

第一,命令行的10°只是一个上层运动参数。

第二,程序会把10°换算成每个轴自己的编码器脉冲数。

第三,真正发给伺服驱动器的是0x607A Target Position,伺服驱动器收到目标位置后,自己闭环控制电机运动。

http://www.jsqmd.com/news/1075841/

相关文章:

  • AI工程师五阶实战路径:从RAG到可信模型交付
  • MATLAB生成BPSK水声通信发射波形:从比特流到WAV文件
  • 贝锐如何赋能餐饮连锁行业?门店“网络+运维”闭环如何构建?
  • 为什么免费开源的 Odoo 比其他 ERP 更适合准备IPO的企业?
  • JWT硬编码密钥漏洞实战:从原理到DataEase身份认证绕过复现
  • 关于位图结构在集合操作中的性能优势与局限的技术7
  • 新闻语义结构化处理协议:面向NLP研究的Cypher流水线
  • jenv:管理多个 Java 版本的命令行工具
  • 汽车质检从人工抽检到AI全检:四种感知技术如何重构制造质量体系
  • 数字孪生体实战指南:打造高保真AI认知镜像
  • Claude 3.5 Sonnet如何让AI编排层‘归零’
  • 2026亚马逊广告优化指南:如何提高大促期间广告ROI?
  • Stimulsoft参数面板自动显示变量Description如何关闭?
  • 提前规划 PCB 布局!一站式 AI 平台打通器件选型、成熟参考方案
  • 如何用开源LibreSignage在3天内搭建专业数字标牌系统
  • EDWARDS YT76-Z2-Z20控制器单元
  • 3步永久免费解锁IDM:开源激活脚本完整使用指南
  • AI漫画翻译APP:MT阅读器,手机一键翻译日漫教程 MT阅读器、AI漫画翻译、漫画翻译APP、漫画OCR识别、日漫翻译工具、手机漫画翻译、AI翻译漫画、安卓漫画阅读器、悬浮窗翻译、漫画OCR软件
  • 如何用Flowframes实现专业级AI视频插帧:新手快速上手指南
  • Sunshine游戏串流服务器:打造个人专属的跨平台云游戏系统
  • vLLM 部署避坑指南,解决 Instinct GPU 上的编译报错与依赖冲突
  • AI回答品牌解释率的自动化评估系统设计
  • TrollInstallerX完整指南:如何在iOS设备上快速安装TrollStore
  • 终极指南:5分钟用Python实现抖音直播数据实时抓取
  • 算力“新中间层”:Token分销模式兴起与商业逻辑重构
  • 2026年|降AI收藏!学长实测10款降AI率软件红黑榜:论文降AI避坑(含免费降低AI率办法)
  • Neo4j Python Driver:图数据库的官方 Python 驱动
  • AI+复合材料/CFD 机器学习+水泥基复合材料+岩土工程
  • 我备份了3年,数据还是全丢了:90%的人都在犯的3个致命错误
  • AI算力与电网适配:从谐波治理到本地惯量增强的工程实践