步进电机驱动完整指南:从 BSP 驱动层配置、TMC2209 UART 通信到运动控制算法(S 曲线启停、抛物线起步、motion_seen 防残留)的全覆盖。
一、BSP 驱动层
1.1 硬件配置
基于 STM32F103RCT6,单 TIM4 四通道驱动四轴步进电机:
flowchart TB
subgraph BSP[BSP 驱动层]
B1[TIM4 四通道PWM]
B2[TMC2209 UART]
end
subgraph ALGO[运动算法]
A1[S 曲线启停]
A2[抛物线起步]
A3[motion_seen 防残留]
end
subgraph CTRL[控制层]
C1[速度规划]
C2[脉冲计数]
end
BSP --> ALGO --> CTRL
style BSP fill:transparent,stroke:#8dc7ff,color:#eaf4ff
style ALGO fill:transparent,stroke:#8dc7ff,color:#eaf4ff
style CTRL fill:transparent,stroke:#8dc7ff,color:#eaf4ff- TIM4:32 位向上计数,ARR 决定脉冲频率
- CH1~CH4:四路独立 OC 比较输出,各驱动一台步进电机
- TMC2209:通过 UART 单线协议配置(细分、电流、微步模式)
- STEP/DIR/EN:各轴独立引脚
1.2 TMC2209 UART 配置
// TMC2209 单线 UART 写操作
static HAL_StatusTypeDef tmc2209_write_reg(uint8_t addr, uint32_t data) {
uint64_t datagram = tmc2209_build_datagram(addr, data, TMC_WRITE);
// 通过 UART TX 引脚发送 80 位数据帧
// 格式: sync(0xFF) + addr(8) + data(32) + crc(8)
// 使用软件 bit-bang 或硬件 UART 单线模式
return tmc2209_send_datagram(datagram);
}
// 关键配置
tmc2209_write_reg(GCONF, 0x00000004); // pdn_disable = 1, enable UART
tmc2209_write_reg(CHOICE, 0x00040100); // microsteps = 16, vsense = 1
tmc2209_write_reg(TPOWERDOWN, 0x00000010); // 自动减电流延迟
1.3 四轴原子化启停
所有轴同时 start/stop 避免时序差导致机械偏移:
typedef struct {
uint32_t arr; // TIM4 ARR 值(决定脉冲频率)
uint32_t pulse_target; // 目标脉冲数
uint32_t pulse_emitted; // 已发出脉冲数
uint8_t step_pin; // STEP 引脚掩码
uint8_t dir_pin; // DIR 引脚
bool running;
} stepper_axis_t;
static stepper_axis_t axes[STEPPER_AXIS_MAX];
void stepper_start_all(uint32_t freq_hz, uint32_t pulses) {
// 1. 计算 ARR = (TIM_CLOCK / (prescaler * freq)) - 1
// 2. 写入所有轴的目标脉冲数
// 3. 统一启动 TIM4
TIM4->ARR = arr_value;
TIM4->CR1 |= TIM_CR1_CEN; // 所有轴同时开始
}
void stepper_stop_all(void) {
TIM4->CR1 &= ~TIM_CR1_CEN; // 所有轴同时停止
}
1.4 轮询驱动(task_once 模式)
void stepper_task_once(void) {
uint32_t sr = TIM4->SR;
for (int i = 0; i < STEPPER_AXIS_MAX; i++) {
if (!axes[i].running) continue;
if (sr & (TIM_SR_CC1IF << i)) { // 该通道捕获/比较事件
HAL_GPIO_TogglePin(axes[i].step_port, axes[i].step_pin);
axes[i].pulse_emitted++;
if (axes[i].pulse_emitted >= axes[i].pulse_target) {
axes[i].running = false;
axes[i].done = true;
}
}
}
}
二、S 曲线启停
2.1 问题
减速段末端脉冲频率极低,如果等待最后几个慢脉冲走完才置 done,会导致明显的停顿感。
2.2 解法:优先置 done
在 task_once 中检查 emitted ≥ target 时立即 stop + done,不等剩余慢脉冲走完。因为末尾段脉冲已慢到不产生实际位移,直接截断完全可接受。
void stepper_task_once(void) {
// ... 正常脉冲输出
if (axes[i].pulse_emitted >= axes[i].pulse_target) {
axes[i].running = false;
axes[i].done = true; // 立刻标记完成,不等最后脉冲
}
}
2.3 注意事项
- 仅适用于减速段末端脉冲极慢的场景
- 需要配合 motion_seen 防残留机制使用(见下节)
- 如果应用要求精确计步(如定位),不建议截断
三、抛物线起步与 motion_seen 防残留
3.1 二次抛物线起步
开环步进电机在启动瞬间容易丢步,二次抛物线软启动通过逐步增加脉冲频率避免:
// 抛物线加速:f(t) = a * t²
// 每 tick 计算当前频率
static uint32_t calc_parabola_freq(uint32_t tick, uint32_t target_freq, uint32_t accel_time_ms) {
float progress = (float)tick / (float)accel_time_ms;
if (progress >= 1.0f) return target_freq;
return (uint32_t)((float)target_freq * progress * progress);
}
3.2 motion_seen 防残留
问题:S 曲线启停中优先置 done 的做法,会让前一次运动的 done 标志残留到下一次。新运动开始时,如果直接检查 done 标志,会误以为上一次运动还没结束。
解法:引入 motion_seen 标志——每次新运动开始时先清 motion_seen,等待第一次真正发出脉冲后再置位。状态机检查时用 motion_seen 而非 done:
static bool motion_seen[STEPPER_AXIS_MAX] = {false};
void stepper_start_new_move(uint8_t axis) {
motion_seen[axis] = false; // 新运动开始,清标志
axes[axis].pulse_emitted = 0;
axes[axis].pulse_target = target;
axes[axis].running = true;
}
void stepper_task_once(void) {
if (/* 发出一次脉冲 */) {
motion_seen[axis] = true; // 确认已开始运动
}
}
// 外部状态机判断运动完成
bool stepper_is_motion_done(uint8_t axis) {
return motion_seen[axis] && !axes[axis].running;
}
3.3 搭配使用
这两个技巧必须一起用:S 曲线优先置 done = 不等末尾慢脉冲;motion_seen = 防止 done 残留影响下一次判据。缺一个就会出现误判。
四、完整集成:驱动 + 控制算法
在实际项目中,BSP 驱动层和运动控制算法层通过统一动作表调度表链接:
// 动作表条目
typedef struct {
uint8_t axis; // 轴号
uint32_t target_pulse; // 目标脉冲数
uint32_t freq_hz; // 目标频率
uint32_t accel_ms; // 加速时间(ms)
stepper_mode_t mode; // S曲线/抛物线/恒速
} action_entry_t;
// 动作表驱动
void action_table_execute(action_entry_t *table, uint8_t count) {
for (int i = 0; i < count; i++) {
if (table[i].mode == STEPPER_MODE_PARABOLA) {
stepper_start_parabola(table[i].axis,
table[i].target_pulse, table[i].freq_hz, table[i].accel_ms);
} else {
stepper_start_s_curve(table[i].axis,
table[i].target_pulse, table[i].freq_hz);
}
}
}
Comments NOTHING