4.18 基于立创·梁山派GD32F470的步进电机驱动实战八拍法控制与定时器精准调速最近在做一个桌面小机械臂的项目需要用梁山派开发板来控制步进电机。刚开始调试的时候电机就“嗡嗡”地震动死活不转折腾了半天才发现是脉冲给得太快了。今天我就把用梁山派GD32F470ZGT6驱动二相四线步进电机的完整过程分享出来从硬件接线到代码实现特别是如何用定时器精准控制速度让你避开我踩过的那些坑。这篇文章适合正在学习嵌入式电机控制的朋友无论你是刚接触梁山派还是对步进电机原理不太清楚跟着步骤走都能让你的电机稳稳转起来。1. 步进电机驱动原理为什么你的电机只震动不转咱们先别急着写代码搞明白步进电机是怎么工作的后面调代码心里才有底。你可以把步进电机想象成一个“数字式”的电机。普通直流电机通电就转转速由电压决定。而步进电机不一样它走一步停一下。你给它一个电脉冲信号它就转动一个固定的角度比如1.8度再给一个脉冲它就再转1.8度。所以它的转速完全取决于你给脉冲的快慢定位精度则由脉冲的总个数决定。这次咱们驱动的二相四线步进电机内部有两个线圈A相和B相每个线圈有两根线引出所以一共四根线。控制它转动的核心就是按一定顺序给这两个线圈通电产生旋转的磁场拖着内部的转子一步一步走。八拍驱动法是让转动更平滑的一种方式。它比简单的四拍法步骤更细相当于走八小步完成一圈四拍是走四大步。它的通电顺序是 【A】-【AB】-【B】-【BA-】-【A-】-【A-B-】-【B-】-【B-A】注意直接拿开发板的GPIO引脚去驱动步进电机线圈是有风险的线圈在通电和断电瞬间会产生反向电动势电流也可能超出引脚承受能力可能会损坏芯片。稳妥的做法是加一个步进电机驱动模块如A4988、DRV8825用开发板的IO口去控制驱动模块。本教程为了聚焦于软件控制逻辑假设你已通过驱动模块连接或使用的小电流电机。最关键的一点为什么代码写好了电机却只震动不转 这是因为你给脉冲的速度太快了上一个脉冲产生的磁场还没把转子“吸”到位下一个脉冲就来了磁场方向又变了转子就在两个位置间来回抖动看起来就是震动。所以我们必须在每个脉冲之间加入合适的延时等待转子稳定到位。这就是后面我们要用定时器中断来实现精准调速的原因。2. 硬件连接与引脚配置首先根据原文的硬件设计我们需要把步进电机的四个控制线连接到梁山派的指定GPIO引脚上。步进电机控制线梁山派 GPIO 引脚APG12BPB9A-PG10B-PB6接线完成后在代码里我们要把这些引脚初始化为推挽输出模式。推挽输出能提供较强的驱动能力输出明确的高电平和低电平。这里我习惯把引脚定义和操作宏放在头文件里这样主程序看起来更清晰。创建一个bsp_stepper_motor.h头文件#ifndef _BSP_STEPPER_MOTOR_H_ #define _BSP_STEPPER_MOTOR_H_ #include gd32f4xx.h // A 相引脚定义 #define AP_RCU RCU_GPIOG #define AP_PORT GPIOG #define AP_PIN GPIO_PIN_12 // A- 相引脚定义 #define AM_RCU RCU_GPIOG #define AM_PORT GPIOG #define AM_PIN GPIO_PIN_10 // B 相引脚定义 #define BP_RCU RCU_GPIOB #define BP_PORT GPIOB #define BP_PIN GPIO_PIN_9 // B- 相引脚定义 #define BM_RCU RCU_GPIOB #define BM_PORT GPIOB #define BM_PIN GPIO_PIN_6 // 引脚操作宏方便设置高低电平 #define AP(X) gpio_bit_write(AP_PORT, AP_PIN, X?SET:RESET) //A #define AM(X) gpio_bit_write(AM_PORT, AM_PIN, X?SET:RESET) //A- #define BP(X) gpio_bit_write(BP_PORT, BP_PIN, X?SET:RESET) //B #define BM(X) gpio_bit_write(BM_PORT, BM_PIN, X?SET:RESET) //B- // 全局控制标志位 extern uint8_t motor_cw_flag; extern uint8_t motor_ccw_flag; // 函数声明 void stepper_motor_config(void); void stepper_motor_timer_config(void); uint16_t get_step_count(void); void set_step_count(uint16_t num); #endif接下来在bsp_stepper_motor.c文件中实现引脚的初始化函数。这个函数的作用就是打开对应GPIO端口的时钟然后把四个引脚都配置成推挽输出模式并且初始状态都设为低电平电机不通电。void stepper_motor_config(void) { // 1. 开启所有用到的GPIO端口时钟 rcu_periph_clock_enable(AP_RCU); // 开启GPIOG时钟 rcu_periph_clock_enable(AM_RCU); // 开启GPIOG时钟 (和A是同一个端口但多次使能无影响) rcu_periph_clock_enable(BP_RCU); // 开启GPIOB时钟 rcu_periph_clock_enable(BM_RCU); // 开启GPIOB时钟 // 2. 配置A (PG12) 为推挽输出上拉速度50MHz gpio_mode_set(AP_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, AP_PIN); gpio_output_options_set(AP_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, AP_PIN); // 3. 配置A- (PG10) gpio_mode_set(AM_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, AM_PIN); gpio_output_options_set(AM_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, AM_PIN); // 4. 配置B (PB9) gpio_mode_set(BP_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, BP_PIN); gpio_output_options_set(BP_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, BP_PIN); // 5. 配置B- (PB6) gpio_mode_set(BM_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, BM_PIN); gpio_output_options_set(BM_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, BM_PIN); // 6. 初始化所有引脚输出低电平电机不通电 AP(0); AM(0); BP(0); BM(0); }3. 核心驱动八拍法正反转代码实现硬件准备好后我们来写让电机转起来的核心逻辑——八拍法。八拍法的精髓就在于那8个不同的通电状态。我们可以用一个表格来清晰地表示顺时针转动时每一步四个引脚的电平拍数ABA-B-二进制 (ABA-B-)十六进制第一拍100010000x08第二拍110011000x0C第三拍010001000x04第四拍011001100x06第五拍001000100x02第六拍001100110x03第七拍000100010x01第八拍100110010x09我们把这一串十六进制数存到数组里就是顺时针的控制码。逆时针就是把这个顺序反过来。在C文件中我们定义这两个控制码数组和全局控制标志uint8_t phasecw[8] {0x08, 0x0c, 0x04, 0x06, 0x02, 0x03, 0x01, 0x09}; // 顺时针 uint8_t phaseccw[8] {0x09, 0x01, 0x03, 0x02, 0x06, 0x04, 0x0c, 0x08}; // 逆时针 uint8_t motor_cw_flag 0; // 顺时针转动标志 uint8_t motor_ccw_flag 0; // 逆时针转动标志 uint16_t step_count 0; // 步数计数器可用于记录位置接下来是实现正反转的函数。函数里用一个静态变量i来记录当前走到八拍中的哪一拍每次调用函数就输出当前拍对应的电平然后i加1到8就回到0。//顺时针转动函数 void motor_cw(void) { static uint8_t i 0; // 静态变量记录当前拍数函数退出后值保留 // 只有当顺时针标志位为1时才动作 if (motor_cw_flag 1) { // 从控制码中提取每一位设置到对应引脚 // (phasecw[i] 3) 0x01 取出最高位A AP((phasecw[i] 3) 0x01); BP((phasecw[i] 2) 0x01); // 取出次高位B AM((phasecw[i] 1) 0x01); // 取出次低位A- BM((phasecw[i] 0) 0x01); // 取出最低位B- i (i 1) % 8; // 指向下一拍循环 step_count; // 步数加一 } } //逆时针转动函数 void motor_ccw(void) { static uint8_t i 0; if (motor_ccw_flag 1) { AP((phaseccw[i] 3) 0x01); BP((phaseccw[i] 2) 0x01); AM((phaseccw[i] 1) 0x01); BM((phaseccw[i] 0) 0x01); i (i 1) % 8; // 防止步数减到0以下 if (step_count 1) step_count 1; else step_count--; } }现在如果你在main函数里循环调用motor_cw()理论上电机会动。但就像开头说的如果调用太快电机只会震动。我们需要一个“节拍器”来定时、均匀地调用这个函数。4. 定时器精准调速告别电机震动我们用定时器5TIMER5来产生一个稳定的2ms中断在这个中断服务函数里更新电机相位这样电机每2ms走一步速度就稳定可控了。先来看定时器的初始化函数这里涉及到一些计算void stepper_motor_timer_config(void) { timer_parameter_struct timer_initpara; // 1. 开启TIMER5的时钟 rcu_periph_clock_enable(RCU_TIMER5); // 2. 配置定时器时钟源。GD32F470的APB1总线时钟是50MHz定时器时钟是其4倍即200MHz。 rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4); timer_deinit(TIMER5); // 复位定时器 // 3. 配置定时器参数 timer_initpara.prescaler 2000 - 1; // 预分频值 timer_initpara.alignedmode TIMER_COUNTER_EDGE; // 边缘对齐模式 timer_initpara.counterdirection TIMER_COUNTER_UP; // 向上计数 timer_initpara.period 200 - 1; // 自动重装载值 timer_initpara.clockdivision TIMER_CKDIV_DIV1; // 时钟分频 timer_initpara.repetitioncounter 0; // 重复计数器高级定时器用 timer_init(TIMER5, timer_initpara); // 4. 配置中断 nvic_irq_enable(TIMER5_DAC_IRQn, 1, 2); // 设置中断优先级 timer_interrupt_enable(TIMER5, TIMER_INT_UP); // 使能更新中断 // 5. 启动定时器 timer_enable(TIMER5); }定时时间计算关键定时器时钟CK_TIMER 200 MHz。预分频器prescaler 2000 - 1所以计数器实际时钟CK_CNT CK_TIMER / 2000 100 KHz即每计数一次耗时10 us。自动重装载值period 200 - 1计数器从0数到199共200个数。因此中断周期T (2000) * (200) / 200,000,000 Hz 0.002 s 2 ms。提示这个2ms的周期是经验值对于很多常见步进电机来说这个速度500步/秒既能保证转动平稳又有一定扭矩。你可以通过修改prescaler或period的值来调整速度。调快比如周期改为1ms要小心电机堵转或丢步调慢比如10ms电机会转得很慢但力量足。定时器中断服务函数很简单就是每隔2ms进来一次调用一次正反转函数void TIMER5_DAC_IRQHandler(void) { // 判断是否是更新中断 if (timer_interrupt_flag_get(TIMER5, TIMER_INT_FLAG_UP) SET) { timer_interrupt_flag_clear(TIMER5, TIMER_INT_FLAG_UP); // 清除中断标志 // 更新电机相位这里两个函数都调用但由标志位决定哪个真正执行 motor_cw(); motor_ccw(); } }你可能会问两个函数都调用电机听谁的这就是前面motor_cw_flag和motor_ccw_flag的作用了。在中断里两个函数都会执行但每个函数内部第一件事就是检查自己的标志位。只有标志位为1的那个函数才会真正去改变引脚电平。另一个函数因为标志位是0直接跳过。这样设计非常简洁只需要在主循环里改标志位就能控制电机的启停和方向。5. 实战整合让电机转起来所有模块都准备好了现在我们在main.c里把它们组合起来实现一个电机正转500ms再反转500ms的循环效果。#include gd32f4xx.h #include systick.h #include bsp_stepper_motor.h int main(void) { uint8_t motor_flag 0; // 1. 系统初始化 nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 设置中断优先级分组 systick_config(); // 初始化系统滴答定时器用于delay // 串口初始化代码如果需要调试可以加上 // usart_gpio_config(9600U); // 2. 步进电机模块初始化 stepper_motor_config(); // 初始化GPIO引脚 stepper_motor_timer_config(); // 初始化定时器开始产生2ms中断 // 3. 主循环每500ms切换一次转向 while (1) { motor_flag !motor_flag; // 状态翻转 if (motor_flag 0) { // 设置为顺时针转动 motor_cw_flag 1; motor_ccw_flag 0; // 可以在这里打印信息printf(CW\r\n); } else { // 设置为逆时针转动 motor_cw_flag 0; motor_ccw_flag 1; // printf(CCW\r\n); } delay_1ms(500); // 等待500ms } }把代码编译下载到梁山派开发板连接好电机和电源务必确认接线正确且电源功率足够应该就能看到步进电机开始平稳地正反转了。常见问题排查电机不转只是震动或发热几乎可以肯定是脉冲太快。检查定时器配置确保中断周期在几毫秒以上可以先试试改成5ms或10ms。电机完全不响检查GPIO初始化是否正确用万用表量一下引脚是否有电平变化。检查控制标志位motor_cw_flag/motor_ccw_flag是否被正确设置为1。转动方向相反只需要把电机A和A-的接线对调或者B和B-对调即可也可以在代码里交换顺时针和逆时针的控制码数组。力量不足容易丢步可能是速度太快或者负载太重。降低速度增大定时器周期或者为电机驱动模块提供更高的电压/电流需在驱动模块允许范围内。这个代码框架已经是一个可用的步进电机驱动核心了。你可以在此基础上扩展功能比如通过按键或串口命令来灵活控制启停、方向、速度动态修改定时器周期和步数设定step_count目标值。