第四节: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,效果刚刚好。

小技巧:步长一般取终点坐标最大值的1/100到1/1000,具体看你系统的脉冲当量。

初始化函数长这样:

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);
        }
    }
}
关键点:累加器减去的不是步长,而是max_steps。这是DDA的精髓——用减法代替除法,硬件实现特别快。

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);
    }
}
注意:脉冲频率不能超过驱动器的最大响应频率。一般步进电机驱动器上限在200kHz左右,伺服驱动器可以到1MHz以上。

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同时动了,那就是代码逻辑有问题。正常情况下,每一步最多只有一个轴输出脉冲。

调试心得:先用小数据量测试,比如从(0,0)到(10,5)。手动算一遍期望的轨迹,再跟程序输出对比。我每次都是这么干的,基本一次过。
DDA插补软件实现流程图 开始 初始化参数 起点、终点、步长、增量 循环:i = 0 到 max_steps 累加器更新 acc_x += delta_x, acc_y += delta_y 溢出判断 X轴输出脉冲 Y轴输出脉冲

这张图把整个流程串起来了。你从顶部开始看,初始化参数后进入循环,每次循环先更新累加器,然后判断哪个轴溢出,就输出对应的脉冲。循环结束,直线就走完了。

说实话,DDA插补的软件实现并不复杂。难的是理解它背后的数学原理——用积分的思想来逼近直线轨迹。你只要把初始化、累加、溢出判断、脉冲输出这四个环节搞明白,剩下的就是调参数的事了。

我在项目里用这套代码,跑过从(0,0)到(1000, 666)的直线,误差不超过1个脉冲。嗯,够用了。


公众号:蓝海资料掘金营,微信deep3321