3. 梯形加减速源码实现(C语言):状态机设计、参数计算、核心代码逐行解析

好,我们直接进入正题。上一章我们把梯形加减速的数学模型讲透了,速度曲线长什么样,位移怎么算,大家心里都有数了。但理论归理论,落到嵌入式平台上,你得用代码把它跑起来。

这一章,我就带着大家手撕一份完整的梯形加减速C语言实现。我会从状态机设计讲起,然后说参数怎么算,最后逐行解析核心代码。嗯,这里面的坑不少,我当年在雕刻机项目上踩过的,今天一并告诉你。

核心要点:梯形加减速的工程实现,本质是一个有限状态机 + 定时中断的配合。状态机管理运动阶段,定时器产生精确的时间基准。

3.1 状态机设计:运动的“大脑”

梯形加减速,说白了就三个阶段:加速、匀速、减速。但实际跑起来,还有“停止”和“空闲”状态。所以,一个完整的运动状态机至少需要5个状态。

我个人习惯用枚举来定义状态,清晰明了,不容易出错。

typedef enum {
    MOTOR_IDLE = 0,      // 空闲,电机未使能或停止
    MOTOR_ACCEL,         // 加速阶段
    MOTOR_RUN,           // 匀速阶段
    MOTOR_DECEL,         // 减速阶段
    MOTOR_STOP           // 停止阶段(减速到0后的短暂状态)
} MotorState_t;

为什么需要 MOTOR_STOP 状态?你想想看,电机从高速突然停下来,如果直接切回 IDLE,可能会丢失最后一步的脉冲。我在项目中遇到过,步进电机在高速急停时,因为惯性会多冲出去几个脉冲,导致定位不准。加一个 STOP 状态,做一次“软着陆”处理,就能解决这个问题。

状态机的跳转逻辑,我用一张图来说明:

IDLE ACCEL RUN DECEL STOP 启动指令 达到目标速度 到达减速点 速度减到0 完成标志 急停/异常

这张图我画得比较清楚。正常流程是 IDLE → ACCEL → RUN → DECEL → STOP → IDLE。但实际工程中,你还要考虑异常情况,比如急停、限位触发,这时候任何状态都要能直接跳转到 STOP 或 IDLE。

小技巧:状态机里加一个 MOTOR_FAULT 状态,专门处理过流、过温等硬件故障。我后来做的所有运动控制项目,都会保留这个状态,安全第一嘛。

3.2 参数计算:把数学公式变成代码

状态机搭好了,接下来要算参数。梯形加减速的核心参数就几个:

  • 起始速度 v0:电机启动时的速度,通常不为0,避免低频共振。
  • 目标速度 vt:匀速阶段的速度。
  • 加速度 a:加速和减速的加速度大小。
  • 总位移 S:电机需要走的总步数或总距离。

有了这些,就能算出加速段位移 Sa、减速段位移 Sd、匀速段位移 Sr。公式上一章讲过了,这里直接上代码:

typedef struct {
    float v0;          // 起始速度 (steps/s)
    float vt;          // 目标速度 (steps/s)
    float a;           // 加速度 (steps/s^2)
    int32_t total_steps; // 总步数
    // 计算出的参数
    float accel_steps; // 加速段步数
    float decel_steps; // 减速段步数
    float run_steps;   // 匀速段步数
    float accel_time;  // 加速时间 (s)
    float decel_time;  // 减速时间 (s)
    float run_time;    // 匀速时间 (s)
} TrapezoidParam_t;

参数计算的函数,我习惯这样写:

int TrapezoidParam_Calc(TrapezoidParam_t *p) {
    if (p == NULL) return -1;
    
    // 1. 计算加速和减速所需步数
    p->accel_steps = (p->vt * p->vt - p->v0 * p->v0) / (2.0f * p->a);
    p->decel_steps = p->accel_steps;  // 加减速对称
    
    // 2. 判断是否能达到目标速度
    if (p->accel_steps + p->decel_steps > p->total_steps) {
        // 达不到目标速度,需要计算实际能达到的最高速度
        // 这是一个三角形速度曲线
        float v_peak = sqrtf((2.0f * p->a * p->total_steps + p->v0 * p->v0) / 2.0f);
        p->vt = v_peak;
        p->accel_steps = p->total_steps / 2.0f;
        p->decel_steps = p->total_steps - p->accel_steps;
        p->run_steps = 0;
    } else {
        // 能达到目标速度,有匀速段
        p->run_steps = p->total_steps - p->accel_steps - p->decel_steps;
    }
    
    // 3. 计算各阶段时间
    p->accel_time = (p->vt - p->v0) / p->a;
    p->decel_time = p->accel_time;
    p->run_time = p->run_steps / p->vt;
    
    return 0;
}

这里有个关键点:判断是否能达到目标速度。如果总步数太少,加速还没完就要减速了,这时候实际速度曲线是三角形,不是梯形。我曾经在点胶机上遇到过这个问题,行程太短,电机一直在加速减速之间切换,根本跑不到目标速度。后来加了这段判断逻辑,问题就解决了。

注意:浮点数运算在嵌入式MCU上比较耗时。如果MCU没有FPU,建议把所有参数放大1000倍用整数运算,或者用Q格式定点数。我一般用 int32_t 配合位移操作,精度够用,速度也快。

3.3 核心代码逐行解析:定时器中断里的“心脏”

参数算好了,状态机也搭好了,接下来就是最核心的部分——在定时器中断里产生精确的脉冲。梯形加减速的实时性要求很高,一般放在定时器中断服务函数里执行。

先定义运动控制的结构体:

typedef struct {
    MotorState_t state;          // 当前状态
    TrapezoidParam_t param;      // 运动参数
    volatile int32_t step_count; // 当前已走步数
    float current_speed;         // 当前速度
    float step_delay;            // 当前步进延时 (us)
    uint32_t timer_period;       // 定时器重装值
} MotorControl_t;

定时器中断服务函数,我逐行解释:

void TIM_IRQHandler(void) {
    if (TIM_GetITStatus(TIMx) == RESET) return;
    
    MotorControl_t *motor = &g_motor;  // 全局运动控制对象
    
    // 1. 状态机处理
    switch (motor->state) {
        case MOTOR_ACCEL:
            // 加速阶段:每来一次中断,速度增加
            motor->current_speed += motor->param.a * TICK_TIME;
            // 检查是否达到目标速度
            if (motor->current_speed >= motor->param.vt) {
                motor->current_speed = motor->param.vt;
                motor->state = MOTOR_RUN;
            }
            break;
            
        case MOTOR_RUN:
            // 匀速阶段:速度不变
            // 检查是否到达减速点
            if (motor->step_count >= (motor->param.total_steps - motor->param.decel_steps)) {
                motor->state = MOTOR_DECEL;
            }
            break;
            
        case MOTOR_DECEL:
            // 减速阶段:每来一次中断,速度减小
            motor->current_speed -= motor->param.a * TICK_TIME;
            // 检查是否减速到0
            if (motor->current_speed <= motor->param.v0) {
                motor->current_speed = motor->param.v0;
                motor->state = MOTOR_STOP;
            }
            break;
            
        case MOTOR_STOP:
            // 停止阶段:关闭定时器,置完成标志
            TIM_Cmd(TIMx, DISABLE);
            motor->state = MOTOR_IDLE;
            // 触发回调函数,通知上层运动完成
            if (g_motion_callback) g_motion_callback(MOTION_DONE);
            break;
            
        default:
            break;
    }
    
    // 2. 根据当前速度计算定时器重装值
    // 速度越快,定时器周期越短,脉冲频率越高
    if (motor->current_speed > 0) {
        motor->step_delay = 1000000.0f / motor->current_speed;  // 单位:us
        motor->timer_period = (uint32_t)(motor->step_delay / TIMER_RESOLUTION);
        TIM_SetAutoreload(TIMx, motor->timer_period);
    }
    
    // 3. 输出脉冲(翻转IO口)
    GPIO_ToggleBits(GPIOx, GPIO_Pin_0);
    motor->step_count++;
    
    TIM_ClearITPendingBit(TIMx, TIM_IT_Update);
}

这段代码,我重点说几个地方:

  • 速度更新:current_speed += param.a * TICK_TIME,这是最朴素的梯形加减速实现。TICK_TIME 是定时器中断的基准时间,比如1ms。
  • 减速点判断:step_count >= total_steps - decel_steps,这个判断很关键。我见过有人用剩余距离来判断,但用已走步数更直观,不容易出错。
  • 定时器重装值计算:速度越快,step_delay 越小,定时器周期越短。这里要注意,step_delay 不能小于定时器的最小分辨率,否则会溢出。

避坑指南:我曾经在减速阶段遇到过一个问题——速度减到0了,但步数还没走完。原因是减速段的步数计算有误差,导致减速点判断早了。后来我在 DECEL 状态里加了一个保护:如果 current_speed <= v0 但步数还没走完,就强制把剩余步数以 v0 速度走完。嗯,这算是一个“补丁”,但工程上很实用。

3.4 完整调用流程

最后,我把整个流程串起来,看看怎么调用:

int main(void) {
    // 初始化硬件
    TIM_Init();
    GPIO_Init();
    
    // 配置运动参数
    MotorControl_t motor;
    motor.param.v0 = 100;        // 起始速度 100 steps/s
    motor.param.vt = 2000;       // 目标速度 2000 steps/s
    motor.param.a = 5000;        // 加速度 5000 steps/s^2
    motor.param.total_steps = 10000; // 总步数 10000
    
    // 计算参数
    TrapezoidParam_Calc(&motor.param);
    
    // 启动运动
    motor.state = MOTOR_ACCEL;
    motor.current_speed = motor.param.v0;
    motor.step_count = 0;
    TIM_Cmd(TIMx, ENABLE);  // 开启定时器,开始产生脉冲
    
    // 主循环做其他事情,或者等待运动完成
    while (1) {
        // 可以在这里查询状态,或者处理其他任务
        if (motor.state == MOTOR_IDLE) {
            // 运动完成
            break;
        }
    }
    
    return 0;
}

你看,整个流程很清晰:配置参数 → 计算参数 → 启动定时器 → 中断里自动跑状态机 → 运动完成。主循环几乎不用管,这就是状态机 + 定时器的好处。

好了,这一章的内容就到这里。梯形加减速的源码实现,核心就是状态机管理阶段,定时器产生脉冲,参数计算保证精度。下一章我们会讲S形加减速,那又是另一番天地了。