2个方向均匀排列步数的算法
Algorithm for evenly arranging steps in 2 directions
我目前正在为 CNC 机器的控制器编程,因此我需要获取从 A 点到 B 点时每个方向的步进电机步数。
例如点 A 的坐标是 x=0 和 y=0,B 的坐标是 x=15 和 y=3。所以我必须在 x 轴上走 15 步,在 y 轴上走 3 步。
但是我如何以一种平滑的方式混合这两个值(也不是先 x 然后 y,这会导致非常难看的线条)?
在我的 x=15 和 y=3 的示例中,我希望它这样排列:
for 3 times do:
x:4 steps y:0 steps
x:1 steps y:1 step
但是我怎样才能从算法中得到这些数字呢?
我希望你明白我的问题是什么,谢谢你的时间,
卢卡
您应该计算每个坐标上的距离之间的比率,然后在沿着距离最长的坐标的步长与在两个坐标上执行单个单位步长的步长之间交替。
这是 JavaScript 中的一个实现 -- 仅使用最简单的语法:
function steps(a, b) {
const dx = Math.abs(b.x - a.x);
const dy = Math.abs(b.y - a.y);
const sx = Math.sign(b.x - a.x); // sign = -1, 0, or 1
const sy = Math.sign(b.y - a.y);
const longest = Math.max(dx, dy);
const shortest = Math.min(dx, dy);
const ratio = shortest / longest;
const series = [];
let longDone = 0;
let remainder = 0;
for (let shortStep = 0; shortStep < shortest; shortStep++) {
const steps = Math.ceil((0.5 - remainder) / ratio);
if (steps > 1) {
if (dy === longest) {
series.push( {x: 0, y: (steps-1)*sy} );
} else {
series.push( {x: (steps-1)*sx, y: 0} );
}
}
series.push( {x: sx, y: sy} );
longDone += steps;
remainder += steps*ratio-1;
}
if (longest > longDone) {
if (dy === longest) {
series.push( {x: 0, y: longest-longDone} );
} else {
series.push( {x: longest-longDone, y: 0} );
}
}
return series;
}
// Demo
console.log(steps({x: 0, y: 0}, {x: 3, y: 15}));
请注意,第一段比其他所有段都短,因此它与序列在第二点附近的结束方式更加对称。如果您不喜欢这样,请将代码中出现的 0.5
替换为 0 或 1。
想想 Bresenham's line drawing algorithm - 他多年前为绘图员发明了它。 (也是DDA之一)
在你的例子中X/Y位移有公约数GCD=3 > 1
,所以步长应该均匀变化,但在一般情况下它们不会分布得如此均匀。
如果您的控制器发出命令的速度比步进电机实际转动的速度快,您可能想要使用某种基于事件驱动的定时器系统。您需要计算 何时 触发每个电机,以便运动在两个轴上均匀分布。
较长的运动应尽可能快地编程(也就是说,如果电机每秒可以执行 100 步,则每 1/100 秒脉冲一次)和其他运动间隔更长。
编辑:以上段落假定您希望尽可能快地移动工具。通常情况并非如此。通常,工具速度是给定的,因此您需要分别计算沿 X 和 Y(也可能是 Z)轴的速度。您还应该知道电机的一步对应的工具移动距离。因此,您可以计算每个时间单位需要执行的步数,以及整个运动的持续时间,以及沿每个轴的连续步进脉冲之间的时间间隔。
因此,您将定时器编程为在计算出的最小时间间隔后启动,为相应的电机提供脉冲,为下一个脉冲编程定时器,依此类推。
这是一种简化,因为电机与所有物理对象一样,具有惯性并且需要时间 accelerate/decelerate。所以如果你想产生平滑的运动,你需要考虑到这一点。还有更多的考虑因素需要考虑。但这更多的是关于物理而不是编程。编程模型保持不变。您将机器建模为以某种已知方式对已知刺激(步进脉冲)做出反应的物理对象。您的程序计算来自模型的步进脉冲的时序,并处于事件循环中,等待下一次事件发生。
这里有两个主要问题:
轨迹
这可以由任何 interpolation/rasterization 处理,例如:
- DDA
- 布雷森纳姆
DDA 是您的最佳选择,因为它可以轻松处理任意数量的维度,并且可以在整数和浮点运算上计算。它也更快(在 x386 时代不是这样,但现在 CPU 架构改变了一切)
即使您只有 2D 机器,插值本身也很可能是多维的,因为您可能会添加其他内容,例如:保持力、工具转速、任何压力等......必须进行插值以同样的方式沿着你的路线。
速度
这个要复杂得多。您需要将电机从开始位置平稳地驱动到结束位置,与这些有关:
- 线路 start/end 速度,因此您可以顺畅地将更多线路连接在一起
- 最高速度(取决于制造过程,通常每个工具都是恒定的)
- motor/mechanics共鸣
- 电机速度限制:start/stop 和 top
当写速度时,我指的是电机步进的频率 [Hz]
或工具的物理速度 [m/s]
或 [mm/2]
。
线性插值对此不利我使用三次方代替,因为它们可以平滑连接并为速度变化提供良好的形状。参见:
插值立方体(CATMUL ROM 的形式)正是我在此类任务中使用的(并且我正是出于这个目的派生的)
主要是电机启动问题。您需要从 0 Hz
驱动到某个频率,但通常的步进电机在较低频率下会产生共振,并且由于多维机器无法避免它们,因此您需要在这些频率上花费尽可能少的时间。还有另一种方法可以通过增加重量或改变形状以及在电机本身上添加惯性阻尼器(仅限旋转电机)
来处理运动学的这种移动共振
单条 start/stop 行的通常速度控制如下所示:
所以你应该有 2 个立方体,每次启动一个,每次停止一个,将你的线分成 2 个连接的线。你必须这样做,这样启动和停止频率是可配置的...
现在如何合并速度和时间?为此,我正在使用 离散非线性时间 :
它是相同的过程,但不是时间而是角度。 sinwave 的频率呈线性变化,因此您需要随三次变化的频率变化。此外,您还没有正弦波,所以不要使用结果 time
作为 DDA 的插值参数……或者将其与下一步的时间进行比较,如果更大或等于,则执行步骤并计算下一个……
这是此技术的另一个示例:
- how to control the speed of animation, using a Bezier curve?
这个实际上做了你应该做的......用三次曲线控制的速度插值 DDA。
完成后,您需要在此之上构建另一层,该层将为每条轨迹线配置速度,因此结果尽可能快,并匹配您的机器速度限制,并在可能的情况下匹配工具速度。这部分是最复杂的...
当我将所有这些放在一起时,为了向您展示前面的内容,我的 CNC 插补器有约 166KByte 的纯 C++ 代码,不算依赖库,如矢量数学、动态列表、通信等...整个控制代码是 ~2.2 MByte
我目前正在为 CNC 机器的控制器编程,因此我需要获取从 A 点到 B 点时每个方向的步进电机步数。 例如点 A 的坐标是 x=0 和 y=0,B 的坐标是 x=15 和 y=3。所以我必须在 x 轴上走 15 步,在 y 轴上走 3 步。 但是我如何以一种平滑的方式混合这两个值(也不是先 x 然后 y,这会导致非常难看的线条)? 在我的 x=15 和 y=3 的示例中,我希望它这样排列:
for 3 times do:
x:4 steps y:0 steps
x:1 steps y:1 step
但是我怎样才能从算法中得到这些数字呢? 我希望你明白我的问题是什么,谢谢你的时间, 卢卡
您应该计算每个坐标上的距离之间的比率,然后在沿着距离最长的坐标的步长与在两个坐标上执行单个单位步长的步长之间交替。
这是 JavaScript 中的一个实现 -- 仅使用最简单的语法:
function steps(a, b) {
const dx = Math.abs(b.x - a.x);
const dy = Math.abs(b.y - a.y);
const sx = Math.sign(b.x - a.x); // sign = -1, 0, or 1
const sy = Math.sign(b.y - a.y);
const longest = Math.max(dx, dy);
const shortest = Math.min(dx, dy);
const ratio = shortest / longest;
const series = [];
let longDone = 0;
let remainder = 0;
for (let shortStep = 0; shortStep < shortest; shortStep++) {
const steps = Math.ceil((0.5 - remainder) / ratio);
if (steps > 1) {
if (dy === longest) {
series.push( {x: 0, y: (steps-1)*sy} );
} else {
series.push( {x: (steps-1)*sx, y: 0} );
}
}
series.push( {x: sx, y: sy} );
longDone += steps;
remainder += steps*ratio-1;
}
if (longest > longDone) {
if (dy === longest) {
series.push( {x: 0, y: longest-longDone} );
} else {
series.push( {x: longest-longDone, y: 0} );
}
}
return series;
}
// Demo
console.log(steps({x: 0, y: 0}, {x: 3, y: 15}));
请注意,第一段比其他所有段都短,因此它与序列在第二点附近的结束方式更加对称。如果您不喜欢这样,请将代码中出现的 0.5
替换为 0 或 1。
想想 Bresenham's line drawing algorithm - 他多年前为绘图员发明了它。 (也是DDA之一)
在你的例子中X/Y位移有公约数GCD=3 > 1
,所以步长应该均匀变化,但在一般情况下它们不会分布得如此均匀。
如果您的控制器发出命令的速度比步进电机实际转动的速度快,您可能想要使用某种基于事件驱动的定时器系统。您需要计算 何时 触发每个电机,以便运动在两个轴上均匀分布。
较长的运动应尽可能快地编程(也就是说,如果电机每秒可以执行 100 步,则每 1/100 秒脉冲一次)和其他运动间隔更长。
编辑:以上段落假定您希望尽可能快地移动工具。通常情况并非如此。通常,工具速度是给定的,因此您需要分别计算沿 X 和 Y(也可能是 Z)轴的速度。您还应该知道电机的一步对应的工具移动距离。因此,您可以计算每个时间单位需要执行的步数,以及整个运动的持续时间,以及沿每个轴的连续步进脉冲之间的时间间隔。
因此,您将定时器编程为在计算出的最小时间间隔后启动,为相应的电机提供脉冲,为下一个脉冲编程定时器,依此类推。
这是一种简化,因为电机与所有物理对象一样,具有惯性并且需要时间 accelerate/decelerate。所以如果你想产生平滑的运动,你需要考虑到这一点。还有更多的考虑因素需要考虑。但这更多的是关于物理而不是编程。编程模型保持不变。您将机器建模为以某种已知方式对已知刺激(步进脉冲)做出反应的物理对象。您的程序计算来自模型的步进脉冲的时序,并处于事件循环中,等待下一次事件发生。
这里有两个主要问题:
轨迹
这可以由任何 interpolation/rasterization 处理,例如:
- DDA
- 布雷森纳姆
DDA 是您的最佳选择,因为它可以轻松处理任意数量的维度,并且可以在整数和浮点运算上计算。它也更快(在 x386 时代不是这样,但现在 CPU 架构改变了一切)
即使您只有 2D 机器,插值本身也很可能是多维的,因为您可能会添加其他内容,例如:保持力、工具转速、任何压力等......必须进行插值以同样的方式沿着你的路线。
速度
这个要复杂得多。您需要将电机从开始位置平稳地驱动到结束位置,与这些有关:
- 线路 start/end 速度,因此您可以顺畅地将更多线路连接在一起
- 最高速度(取决于制造过程,通常每个工具都是恒定的)
- motor/mechanics共鸣
- 电机速度限制:start/stop 和 top
当写速度时,我指的是电机步进的频率
[Hz]
或工具的物理速度[m/s]
或[mm/2]
。线性插值对此不利我使用三次方代替,因为它们可以平滑连接并为速度变化提供良好的形状。参见:
插值立方体(CATMUL ROM 的形式)正是我在此类任务中使用的(并且我正是出于这个目的派生的)
主要是电机启动问题。您需要从
来处理运动学的这种移动共振0 Hz
驱动到某个频率,但通常的步进电机在较低频率下会产生共振,并且由于多维机器无法避免它们,因此您需要在这些频率上花费尽可能少的时间。还有另一种方法可以通过增加重量或改变形状以及在电机本身上添加惯性阻尼器(仅限旋转电机)单条 start/stop 行的通常速度控制如下所示:
所以你应该有 2 个立方体,每次启动一个,每次停止一个,将你的线分成 2 个连接的线。你必须这样做,这样启动和停止频率是可配置的...
现在如何合并速度和时间?为此,我正在使用 离散非线性时间 :
它是相同的过程,但不是时间而是角度。 sinwave 的频率呈线性变化,因此您需要随三次变化的频率变化。此外,您还没有正弦波,所以不要使用结果
time
作为 DDA 的插值参数……或者将其与下一步的时间进行比较,如果更大或等于,则执行步骤并计算下一个……这是此技术的另一个示例:
- how to control the speed of animation, using a Bezier curve?
这个实际上做了你应该做的......用三次曲线控制的速度插值 DDA。
完成后,您需要在此之上构建另一层,该层将为每条轨迹线配置速度,因此结果尽可能快,并匹配您的机器速度限制,并在可能的情况下匹配工具速度。这部分是最复杂的...
当我将所有这些放在一起时,为了向您展示前面的内容,我的 CNC 插补器有约 166KByte 的纯 C++ 代码,不算依赖库,如矢量数学、动态列表、通信等...整个控制代码是 ~2.2 MByte