第四节:DDA插补的软件实现
好,咱们今天来聊聊DDA插补到底怎么写代码。说实话,我刚入行那会儿,看到「数字积分法」这几个字,第一反应是——这玩意儿是不是得用FPGA?后来才发现,用单片机一样能跑,而且逻辑特别清晰。
4.1 初始化参数:起点、终点、步长
写DDA插补的第一步,就是把参数准备好。说白了,你得告诉程序:从哪儿开始,到哪儿去,每一步走多长。
我个人习惯用结构体来封装这些参数,这样代码看着清爽,也方便后续扩展。
typedef struct {
int32_t x_start; // 起点X
int32_t y_start; // 起点Y
int32_t x_end; // 终点X
int32_t y_end; // 终点Y
int32_t step; // 步长(脉冲当量)
int32_t delta_x; // X方向增量
int32_t delta_y; // Y方向增量
int32_t max_steps; // 总步数
} DDA_Line_t;
这里有个坑,我踩过。步长这个参数,很多人直接设成1,觉得越小越精细。但你想想看,步长太小,累加器溢出就慢,电机跑起来跟蜗牛似的。我在做一台雕刻机时,步长设成10,效果刚刚好。
初始化函数长这样:
void DDA_Init(DDA_Line_t *line, int32_t xs, int32_t ys,
int32_t xe, int32_t ye, int32_t step) {
line->x_start = xs;
line->y_start = ys;
line->x_end = xe;
line->y_end = ye;
line->step = step;
line->delta_x = xe - xs;
line->delta_y = ye - ys;
// 总步数取绝对值较大的方向
int32_t abs_dx = abs(line->delta_x);
int32_t abs_dy = abs(line->delta_y);
line->max_steps = (abs_dx > abs_dy) ? abs_dx : abs_dy;
}
4.2 循环累加与溢出判断
DDA的核心思想,就是两个累加器同时跑。X方向累加器加delta_x,Y方向累加器加delta_y。谁先溢出,谁就走一步。
嗯,这里要注意:溢出判断用的是「累加值 ≥ 总步数」这个条件。为什么不是大于?因为等于的时候也得走,不然最后一步就丢了。我曾经因为这个bug,画出来的直线总是差一个像素,排查了一下午。
void DDA_Run(DDA_Line_t *line) {
int32_t acc_x = 0; // X方向累加器
int32_t acc_y = 0; // Y方向累加器
int32_t current_x = line->x_start;
int32_t current_y = line->y_start;
for(int i = 0; i < line->max_steps; i++) {
acc_x += line->delta_x;
acc_y += line->delta_y;
// 溢出判断
if(acc_x >= line->max_steps) {
acc_x -= line->max_steps;
current_x += (line->delta_x > 0) ? 1 : -1;
// 输出X方向脉冲
OutputPulse(X_AXIS);
}
if(acc_y >= line->max_steps) {
acc_y -= line->max_steps;
current_y += (line->delta_y > 0) ? 1 : -1;
// 输出Y方向脉冲
OutputPulse(Y_AXIS);
}
}
}
4.3 输出脉冲序列
脉冲输出这事儿,说白了就是控制IO口的高低电平。但实际项目中,脉冲的宽度和频率都有讲究。
我记得有一次,客户反映电机运行时噪音特别大。查了半天,发现是脉冲宽度太窄,驱动器没反应过来。后来我把脉宽从2us调到10us,世界安静了。
void OutputPulse(uint8_t axis) {
if(axis == X_AXIS) {
HAL_GPIO_WritePin(X_STEP_GPIO_Port, X_STEP_Pin, GPIO_PIN_SET);
delay_us(PULSE_WIDTH); // 脉宽可调
HAL_GPIO_WritePin(X_STEP_GPIO_Port, X_STEP_Pin, GPIO_PIN_RESET);
} else {
HAL_GPIO_WritePin(Y_STEP_GPIO_Port, Y_STEP_Pin, GPIO_PIN_SET);
delay_us(PULSE_WIDTH);
HAL_GPIO_WritePin(Y_STEP_GPIO_Port, Y_STEP_Pin, GPIO_PIN_RESET);
}
}
4.4 单步调试与波形观察
写代码容易,调代码难。DDA插补最怕的就是直线走歪了。我的调试方法很简单——单步执行,看波形。
具体做法是:在循环里加个断点,每走一步停一下。用逻辑分析仪抓X和Y的脉冲波形。理想的波形应该是:
- X和Y脉冲交替出现,不会同时输出
- 脉冲间隔均匀,没有突发的密集脉冲
- 总脉冲数等于max_steps
我一般会在代码里加个调试模式:
void DDA_Debug(DDA_Line_t *line) {
printf("=== DDA Debug Mode ===\n");
printf("Start: (%d, %d) -> End: (%d, %d)\n",
line->x_start, line->y_start,
line->x_end, line->y_end);
printf("Delta: (%d, %d), Steps: %d\n",
line->delta_x, line->delta_y,
line->max_steps);
int32_t acc_x = 0, acc_y = 0;
int32_t x = line->x_start, y = line->y_start;
for(int i = 0; i < line->max_steps; i++) {
acc_x += line->delta_x;
acc_y += line->delta_y;
printf("Step %3d: acc_x=%4d, acc_y=%4d", i, acc_x, acc_y);
if(acc_x >= line->max_steps) {
acc_x -= line->max_steps;
x += (line->delta_x > 0) ? 1 : -1;
printf(" [X->%d]", x);
}
if(acc_y >= line->max_steps) {
acc_y -= line->max_steps;
y += (line->delta_y > 0) ? 1 : -1;
printf(" [Y->%d]", y);
}
printf("\n");
}
printf("Final position: (%d, %d)\n", x, y);
}
你看,每步的累加器值和坐标变化都打印出来了。如果发现某一步X和Y同时动了,那就是代码逻辑有问题。正常情况下,每一步最多只有一个轴输出脉冲。
这张图把整个流程串起来了。你从顶部开始看,初始化参数后进入循环,每次循环先更新累加器,然后判断哪个轴溢出,就输出对应的脉冲。循环结束,直线就走完了。
说实话,DDA插补的软件实现并不复杂。难的是理解它背后的数学原理——用积分的思想来逼近直线轨迹。你只要把初始化、累加、溢出判断、脉冲输出这四个环节搞明白,剩下的就是调参数的事了。
我在项目里用这套代码,跑过从(0,0)到(1000, 666)的直线,误差不超过1个脉冲。嗯,够用了。
公众号:蓝海资料掘金营,微信deep3321