1. 项目背景与痛点解析
玩过步进电机的朋友都深有体会——当电机突然启动或急停时,整个工作台都会跟着"跳舞"。这种机械震动不仅会产生恼人的噪音,长期使用还会导致传动部件磨损、定位精度下降等实际问题。传统梯形加减速算法虽然实现简单,但在加速度突变点(即梯形拐角处)会产生机械冲击,就像开车时突然踩死油门或刹车的顿挫感。
我在去年一个雕刻机项目中就吃过这个亏:当Z轴以3000脉冲/秒的速度急停时,主轴刀具竟然在工件表面留下了肉眼可见的振动纹路。后来改用S型曲线算法后,同样工况下表面粗糙度直接提升了2个等级。今天我就以STM32F103为例,拆解如何用C语言实现这种"丝滑"的运动控制。
2. S型曲线算法核心原理
2.1 运动学模型分析
S型曲线的精髓在于加速度的连续变化。其运动学模型分为7个阶段:
- 加加速阶段(Jerk>0)
- 匀加速阶段(Jerk=0)
- 减加速阶段(Jerk<0)
- 匀速阶段
- 加减速阶段(Jerk<0)
- 匀减速阶段(Jerk=0)
- 减减速阶段(Jerk>0)
这种三阶导数(加加速度Jerk)连续的特性,使得电机运动就像老司机开手动挡:起步时慢慢抬离合,换挡时转速平顺衔接,刹车时先轻踩再逐渐加力。
2.2 关键参数计算
在STM32上实现时需要预计算以下参数:
c复制typedef struct {
uint32_t max_speed; // 最大脉冲频率(Hz)
uint32_t accel; // 加速度(pulse/s²)
uint32_t jerk; // 加加速度(pulse/s³)
uint32_t total_steps; // 总步数
} MotorProfile;
以1.8°步进电机(200步/转)为例,假设需要移动10mm(丝杠导程5mm/转):
c复制MotorProfile my_motor = {
.max_speed = 3000, // 约90rpm
.accel = 10000, // 0-3000Hz用0.3s
.jerk = 50000, // 加速度变化率
.total_steps = 400 // 10mm/(5mm/转)*200步
};
注意:jerk值过大会失去S曲线效果,过小则延长运动时间。建议初始设为加速度的5-10倍。
3. STM32F103具体实现
3.1 定时器配置
使用TIM2作为脉冲发生器,TIM3用于速度曲线计算:
c复制void TIM_Config(void) {
TIM_TimeBaseInitTypeDef TIM_InitStruct;
// TIM2输出PWM脉冲
TIM_InitStruct.TIM_Prescaler = 72 - 1; // 1MHz计数频率
TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_InitStruct.TIM_Period = 1000 - 1; // 初始1kHz
TIM_InitStruct.TIM_ClockDivision = 0;
TIM_TimeBaseInit(TIM2, &TIM_InitStruct);
// TIM3用于速度规划(100us中断)
TIM_InitStruct.TIM_Prescaler = 7200 - 1; // 10kHz
TIM_InitStruct.TIM_Period = 1 - 1;
TIM_TimeBaseInit(TIM3, &TIM_InitStruct);
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
}
3.2 速度曲线生成算法
在TIM3中断中实时计算下一时刻的频率:
c复制void TIM3_IRQHandler(void) {
static uint32_t phase = 0;
static float current_speed = 0;
static float current_accel = 0;
if(TIM_GetITStatus(TIM3, TIM_IT_Update)) {
// 根据运动阶段更新加速度
switch(get_current_phase()) {
case ACCEL_UP:
current_accel += jerk * 0.0001f; // 100us间隔
break;
case ACCEL_CONST:
// 加速度保持不变
break;
case ACCEL_DOWN:
current_accel -= jerk * 0.0001f;
break;
// 其他阶段类似处理...
}
// 更新速度并设置TIM2
current_speed += current_accel * 0.0001f;
TIM2->ARR = (uint16_t)(1000000.0f / current_speed) - 1;
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
}
3.3 运动阶段状态机
用枚举定义7个运动阶段,通过已走步数判断阶段切换:
c复制typedef enum {
ACCEL_UP, // 加加速
ACCEL_CONST, // 匀加速
ACCEL_DOWN, // 减加速
CRUISE, // 匀速
DECEL_UP, // 加减速
DECEL_CONST, // 匀减速
DECEL_DOWN // 减减速
} MotionPhase;
MotionPhase get_current_phase(uint32_t step_count) {
uint32_t accel_steps = (max_speed*max_speed)/(2*accel);
if (step_count < accel_steps/3) return ACCEL_UP;
else if (step_count < 2*accel_steps/3) return ACCEL_CONST;
// 其他条件判断...
}
4. 实测效果与调参技巧
4.1 振动对比测试
使用手机加速度传感器测量不同算法下的振动幅值:
| 算法类型 | 最大加速度(g) | 达到稳态时间(ms) |
|---|---|---|
| 梯形加减速 | 0.78 | 120 |
| S型加减速 | 0.21 | 180 |
虽然S型算法多用60ms完成运动,但振动幅值降低73%,这对高精度设备至关重要。
4.2 参数调试经验
-
Jerk值黄金法则:先设jerk=accel,然后观察电机运动声音。理想状态应听到平滑的"嗡——"声,若有"咯噔"异响说明jerk太小。
-
实时调参技巧:在TIM3中断中加入以下代码,可通过串口实时调整参数:
c复制if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE)) {
char cmd = USART_ReceiveData(USART1);
if(cmd == 'a') accel += 100; // 增大加速度
// 其他按键处理...
}
- 脉冲丢失应对:在关键位置添加编码器校验,发现丢步时自动进入位置补偿模式:
c复制void TIM4_IRQHandler(void) { // 编码器计数
static int last_pos = 0;
int current_pos = TIM_GetCounter(TIM4);
if(abs(current_pos - last_pos) > 2) {
trigger_compensation(); // 触发补偿
}
last_pos = current_pos;
}
5. 常见问题排查
5.1 电机出现不规则抖动
可能原因及解决方案:
- 定时器中断冲突:确保TIM3中断优先级高于其他运动相关中断
- 计算溢出:将current_speed改为uint32_t类型
- 电源干扰:在电机驱动电源端并联1000uF+0.1uF电容
5.2 运动终点过冲
典型表现为停止后电机继续轻微转动:
- 检查total_steps是否包含加减速段步数
- 在DECEL_DOWN阶段最后10%步数时逐步将jerk减半
- 增加软件限位开关检测
5.3 高速运行时丢步
当max_speed超过3000Hz时可能出现:
- 将TIM2预分频改为36-1(2MHz计数频率)
- 在电机驱动端串联100Ω电阻抑制信号振铃
- 改用带有微步细分功能的驱动器
这个算法我在3D打印机和激光雕刻机上实测过,效果比很多厂家的闭源方案还要好。特别是做圆弧插补时,S型算法能让拐角处的光斑均匀度提升明显。后来我们还衍生出变Jerk值的自适应算法,不过那就是另一个故事了。