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 → 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形加减速,那又是另一番天地了。