STM32F103C8T6驱动步进电机:8拍时序与加减速控制实战

发布时间:2026/9/17 0:23:11
STM32F103C8T6驱动步进电机:8拍时序与加减速控制实战 简介面向STM32初学者的步进电机控制工程包基于STM32F103C8T6实现四线八拍步进电机的完整驱动适合正在学习STM32中断、GPIO与电机驱动的学生或开发者。工程通过外部中断完成加减速与启停控制并借助LED实时显示当前速度等级能帮助读者理解步进电机相序切换、中断嵌套以及运行状态管理。压缩包共9个文件包含5个C源码、2个头文件、1份说明文档和1个引脚定义文本整体仅10KB结构精简便于快速阅读和移植。代码模块涵盖外部中断配置、步进核心逻辑、速度显示驱动函数引用统一整理在核心头文件中可按需拆分LED显示部分需配合74LS138译码器使用适合练习译码电路与GPIO协同控制。同时提供引脚定义和工程框架说明方便接线对照与二次开发。目前已有303人学习下载适合作为课程设计、小车/机械臂项目或电机控制入门调试的参考。1. 一片F103C8T6把8拍步进电机跑顺的完整思路手头有一块STM32F103C8T6最小系统板想驱动42步进电机或者那种常见的28BYJ-48最先遇到的问题往往不是电机本身而是“IO不够用、时序对不上、加减速一抖就失步”。这块芯片是ARM Cortex-M3内核72MHz主频GPIO有PA0-PA15、PB0-PB15、PC13-PC15等实际上算下来能用的普通IO就37个左右。驱动4线步进电机需要4个IO再留几个IO做按键、LED显示完全够用。这篇文章基于“STM32-stepmotor-code”这套源码从4线8拍控制时序讲起到外部中断控制加减速、启停再到用74LS138译码器扩展LED显示速度等级全程给出可直接抄的代码和接线表。适合正在用STM32F103C8T6做小型运动控制、想搞懂步进电机底层时序或者准备把这类方案移植到国产替代芯片上的开发者。2. 4线8拍步进电机工作原理与引脚分配2.1 8拍的相位表不是背出来的步进电机靠绕组按顺序通电产生磁场牵引转子转动。4线电机通常叫两相步进电机有A、A-、B、B-四个端子。单拍驱动4拍每步只切换一个绕组力矩小振动大双拍驱动4拍每次两个绕组同时通电力矩大但分辨率不变。8拍驱动是在单拍和双拍之间交替插入状态比如A → AB → B → BA- → A- → A-B- → B- → B-A每个脉冲对应转子转半步所以叫“8拍”也叫半步驱动。它能保留双拍驱动的力矩又让每步角度减半运行更平滑。STM32这里用的4线8拍实际是把4个IO按照下面的相位状态循环拉高拉低。代码里的step_motor.c核心就是把这一组序列写进一个数组// step_motor.c 片段 // 8拍时序: A, AB, B, BA-, A-, A-B-, B-, B-A const uint8_t phase_table[8][4] { {1,0,0,0}, // A {1,1,0,0}, // AB {0,1,0,0}, // B {0,1,1,0}, // BA- {0,0,1,0}, // A- {0,0,1,1}, // A-B- {0,0,0,1}, // B- {1,0,0,1}, // B-A (实际用 B-A 或 AB- 视接线) };表格中4列分别对应GPIO 4个引脚1表示该引脚输出高电平0表示低电平。注意最后一项不是死循环里的一个固定状态而是为了让下一轮第一个状态“A”能平滑衔接把B-保持高同时A提前拉高。如果顺序反了电机反转只需要把数组遍历方向反过来。很多初学者直接把phase_table按for(i0;i8;i)顺序刷结果电机震动不转大概率是相位顺序和驱动器ULN2003、DRV8825等的输入极性不匹配。用ULN2003驱动28BYJ-48时4个输入IN1-IN4分别对应蓝、粉、黄、橙我的习惯是先在程序里只让单个相位翻转用手捏住电机轴感受“一顿一顿”确认每一拍确实有响应再跑完整8拍。2.2 GPIO引脚分配与最小系统板上的走线STM32F103C8T6最小系统板引脚功能很灵活但不能乱接。比如PA11、PA12是USB专用引脚如果要用USB下载就别复用PB2是BOOT1上电时电平会决定启动模式如果它被外部电路拉低会导致无法重新烧录所以我也尽量避开。这套源码里的stm32引脚定义.txt给出了推荐的分配方案我通常按照下面的表来接功能引脚说明电机APA0TIM2_CH1也可复用此处当普通IO电机A-PA1同上电机BPA2同上电机B-PA3同上加速按键PB0外部中断EXTI0减速按键PB1外部中断EXTI1启停按键PB10外部中断EXTI1074LS138 A0PB12译码器低位74LS138 A1PB13译码器中间位74LS138 A2PB14译码器高位74LS138 G1PB15译码器使能端需置高注意电机IO不能直接接电机。STM32的IO最大灌电流20mA左右而28BYJ-48单相绕组静态电流约150mA4线同时通电可能到600mA必须通过ULN2003、L298N或专用驱动芯片。如果用的是DRV8825这类细分驱动器STM32那4个IO是去接STEP和DIR的就不再是8拍时序了。这很容易混8拍时序是给“整步/半步驱动方式”的DRV8825内部用PWM斩波只收STEP脉冲。源码场景明确是4线8拍那我们假定驱动级是ULN2003或类似达林顿管阵列。2.3 电源和电流回路是隐蔽的坑我的经验是USB线直接给最小系统板供电再给电机供电常常导致单片机复位。因为电机负载变化会在共用GND上产生几伏的毛刺MCU电源瞬间跌落。正确做法是电机电源独立比如5V 1A以上的适配器或者12V接ULN2003的VM。并且电机电源GND和STM32的GND必须共地否则控制信号没有回流路径电机不会转还可能烧驱动。这套源码里没有画电源树但port.c里如果看GPIO模式配置就能看出哪些引脚是推挽输出、哪些是上拉输入。驱动电机那4个引脚应该配置成推挽输出按键引脚配置成上拉输入因为按键另一端接地。我给这套方案做过一个简单计算假设8拍频率是600Hz每拍1.6ms左右一个完整8拍周期就是约13ms对应步进频率600Hz电机转速约每秒75个8拍周期也就是相对低频运行。这适合扭矩优先的场景比如3D打印机调平、云台初始定位。如果要把速度拉高到3kHz以上8拍数组遍历开销不大但定时器中断频率和GPIO翻转速度需要重新评估。3. 外部中断实现加减速与启停控制3.1 按键消抖与外部中断状态机源码里exti.c用了外部中断EXTI0、EXTI1、EXTI10分别对应加速、减速、启停三个按键。外部中断回调里直接处理按键没有用轮询扫描这样好处是MCU在空闲时进入睡眠都能被唤醒坏处是机械按键抖动会产生多次触发。常见做法是加入延时消抖但延时放在中断处理里会让定时中断抖动。我的处理办法是把“抖动状态”用一个简单状态机过滤// exti.c 片段 static uint8_t key_filter 0; void EXTI0_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line0) ! RESET) { // 过滤抖动连续两次中断间隔大于50ms才认为是有效按键 uint32_t now TIM_GetCounter(TIM2); // 借用一个自由运行定时器 if(now - last_time[0] 50) { step_motor_speed_up(); // 调用加速函数 last_time[0] now; } EXTI_ClearITPendingBit(EXTI_Line0); } }这里用TIM2作为时间基准没有额外占用SysTick。last_time是static数组保存每个按键上次有效触发的时间戳。TIM2的计数器是16位的如果按1ms累加溢出周期是65.536ms所以比较差值时要先判断溢出或者直接把比较值限制在合理的范围。这个细节容易被忽略now - last_time写的时候没问题但只有在差值小于65535时才正确。更稳妥的做法是用一个8位或16位的tick变量每次溢出时取反差值用无符号减法自然回绕。3.2 速度等级变量与边界限制加减速的本质是改变定时器中断的频率也就是改变8拍里每一拍的持续时间。源码里step_motor.h中定义了一个速度等级变量// step_motor.h 片段 typedef struct { uint8_t current_level; // 当前速度等级 0~8 uint8_t target_level; // 目标速度等级 uint16_t step_delay; // 每拍延时, 单位us, 由等级决定 } motor_speed_t; extern motor_speed_t g_motor_speed; #define SPEED_LEVEL_MAX 8 #define SPEED_LEVEL_MIN 1step_delay的计算用查表法比直接计算快得多。假设最高等级是3000Hz最低是100Hz对应每拍延时300us到5000us。查表如下等级步频(Hz)每拍延时(us)用途11208333启动/停止微调21805555低速定位32603846云台转动43802631传送带低速55501818常规出料68001250快速抓取71200833高速打印81600625极限速度如果直接用speed_up()把等级从1直接调到8步进电机会瞬间要求跟上1600Hz的脉冲实际转子还没加速到位就会失步甚至堵转。所以step_motor.c里speed_up()函数只递增target_level真正生效要到每拍结束时的speed_update()里按斜坡处理// step_motor.c 片段 void speed_update(void) { if(g_motor_speed.current_level g_motor_speed.target_level) { g_motor_speed.current_level; } else if(g_motor_speed.current_level g_motor_speed.target_level) { g_motor_speed.current_level--; } g_motor_speed.step_delay delay_table[g_motor_speed.current_level]; }这样每拍只能改变一个等级自然形成线性加减速不会因为一格速度差太大而失步。如果你想让加速更平滑可以把current_level改为按时间分段的“梯形加速”比如每20ms加1级用另一个计时变量控制。3.3 定时器中断生成8拍脉冲流真正产生相位的动作在定时器中断里做。以TIM3为例设置ARR寄存器的值来控制周期// step_motor.c 片段 void TIM3_IRQHandler(void) { if(TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); if(g_motor_enable) // 启停标志 { step_motor_phase_next(); // 切换到下一拍 // 重新装载定时器周期 TIM_SetAutoreload(TIM3, g_motor_speed.step_delay); } } }step_motor_phase_next()的动作很简单把phase_index移到下一格然后把这个格的4个值写到对应GPIO的ODR寄存器。如果8拍数组顺序是phase_table[8][4]那GPIO赋值可以用下面的函数void step_motor_phase_set(uint8_t phase_index) { uint8_t i; uint16_t pin_value 0; const uint8_t *state phase_table[phase_index]; for(i0; i4; i) { if(state[i]) { pin_value | motor_pins[i]; // 置位 } } GPIO_Write(GPIOA, pin_value); // 只影响低8位需要掩码保护 }注意GPIO_Write会把整个GPIOA的ODR覆盖掉如果PA4-PA7还接了别的东西就不能这么写。最安全的做法是GPIOA-BRRR和BSRR分离操作先用GPIOA-BRRR 0x000F清零低4位再GPIOA-BSRR pin_value置位需要的位。源码的port.c如果用了标准外设库多半就是这两个寄存器。建议自己改写成BSRR/BRRR避免影响同端口的其他引脚。4. LED速度显示与74LS138译码器的配合4.1 译码器为什么省IOSTM32F103C8T6引脚虽然够用但如果你还要接I2C、USART、SPIIO就会紧张。源码里速度等级8档如果直接接8个LED就要占用8个IO。用74LS138三八译码器只需要3个IO就能选通8个LED中任意一个另加1个使能脚。74LS138的真值表是当G1为高、G2A和G2B为低时输出Y0-Y7中只有对应C-B-A组合的那个引脚为低电平其他为高。LED可以接成灌电流方式即LED负极接译码器输出正极串电阻接VCC当Y输出低时LED点亮。这正好与“亮灯指示当前速度等级”的用途匹配。led.c里应该有一个函数把速度等级映射到三个引脚的电平// led.c 片段 void led_show_level(uint8_t level) { // level: 0~7 GPIO_WriteBit(GPIOB, GPIO_Pin_12, (level 0x01) ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOB, GPIO_Pin_13, (level 0x02) ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOB, GPIO_Pin_14, (level 0x04) ? Bit_SET : Bit_RESET); }这里把速度等级直接当作译码地址。如果速度等级只有1-8而LED显示0-7那么等级8时应该取模显示0或者用第8个LED做成超速警告。我更推荐的做法是等级1-7直接映射到Y0-Y6等级8映射到Y7同时用另一个LED做“FULL”提醒。不过源码里既然限定了8个速度等级也可以直接level 0x07取低3位。4.2 译码器与MCU的电平兼容74LS138是TTL器件输入高电平最小2V而STM32的GPIO输出高电平约3.3V可以直接驱动。但要注意74LS138的VCC是5VSTM32的IO口耐压是5V容忍FT引脚PB12、PB13、PB14、PB15都是FT引脚所以可以直连。如果用的是74HC138工作于3.3V也可以但必须保证VCC3.3V否则5V供电时输入高电平阈值可能高于3.3V导致不识别。这是大家常踩的坑用5V供电的74HC138配3.3V单片机逻辑不稳定。选型时要明确芯片是LS还是HC系列。在led.c里我还会加一个锁存逻辑防止中断里调用led_show_level()导致GPIO毛刺。因为led_show_level不是原子操作三根线依次赋值中间可能短暂出现一个错误的组合译码器会瞬间点亮一个不该亮的LED。建议把三个引脚的位操作组合成一个16位值然后一次性写入void led_show_level(uint8_t level) { uint16_t bits 0; if(level 0x01) bits | GPIO_Pin_12; if(level 0x02) bits | GPIO_Pin_13; if(level 0x04) bits | GPIO_Pin_14; GPIOB-BSRR bits; // 置位 GPIOB-BRRR bits ^ 0x7000; // 清除其他位0x7000Pin12|Pin13|Pin14 }GPIOB-BRRR bits ^ 0x7000是反码操作会把非目标位置零。不过要注意BRRR寄存器写1才清零所以bits ^ 0x7000的结果里为1的位会被清掉为0的位保持原状态。这里需要保证bits是Pin12、13、14组合且没有其他位否则异或计算会乱。更清晰的写法是GPIOB-BRRR 0x7000;先把三个引脚全拉低然后GPIOB-BSRR bits;需要亮的引脚再拉高。两步之间有微小间隙但74LS138输出需要几十ns稳定实际看不出闪烁可用。4.3 初始化顺序和引脚模式检查PORT.c中初始化要注意顺序先打开GPIO时钟再设置GPIO模式最后才操作输出。如果先对未开时钟的GPIO写寄存器写入无效程序不会报错但电机不动。建议检查初始化代码// port.c 片段 void PORT_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // 电机引脚 PA0-PA3 推挽输出 50MHz GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 按键 PB0/PB1 上拉输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOB, GPIO_InitStructure); // 按键 PB10 上拉输入注意PB10与USART3冲突不冲突时用 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_Init(GPIOB, GPIO_InitStructure); // 74LS138 输出 A0 A1 A2 G1 GPIO_InitStructure.GPIO_Pin GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_2MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 把译码器使能端设为高 GPIO_SetBits(GPIOB, GPIO_Pin_15); }GPIO速度设置2MHz足够因为译码器输入只是几MHz的逻辑信号没必要50MHz还能减少EMI。电机引脚设50MHz是因为每次切换相位时电流上升沿速度会影响步进电机高低速性能推挽输出频率太慢会导致波形变圆、转矩下降。4.4 在Keil MDK下编译工程源码解压后的STM32-stepmotor-code.zip包含stm32f10x_conf.h、step_motor.c、exti.c、port.c、main.c等。用Keil MDK打开工程前先检查芯片型号选择是否为STM32F103C8T6这个芯片Flash是64KB如果代码超过64KB会链接报错。C8T6的RAM是20KB而C8T6和CB T6引脚兼容但Flash不同如果不小心选了CB T6烧录后可能正常但以后换芯片会踩容量问题。stm32f10x_conf.h里的#define控制了标准外设库包含哪些模块// stm32f10x_conf.h 片段 #define _GPIO #define _TIM #define _EXTI #define _RCC如果缺少_TIM使用TIM3的代码编译报undefined检查这个文件即可。main.c中一般会有系统时钟初始化和外设初始化调用// main.c 片段 int main(void) { SystemInit(); PORT_Init(); step_motor_init(); exti_init(); led_init(); // 使能外部中断对应的NVIC NVIC_EnableIRQ(EXTI0_IRQn); NVIC_EnableIRQ(EXTI1_IRQn); NVIC_EnableIRQ(EXTI10_IRQn); NVIC_EnableIRQ(TIM3_IRQn); TIM_Cmd(TIM3, ENABLE); while(1) { // 空闲循环可加入串口调试输出 } }exti_init()里特别注意要为每个外部中断线配置EXTI源和GPIO复用映射。比如PB1要映射到EXTI1线调用GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1)否则中断不触发。这段代码容易漏漏了之后按键没反应检查顺序就是先确认GPIO时钟与输入模式再确认EXTI_LINE与引脚一致最后确认NVIC优先级分组。5. 加减速斜率与失步排查还有74LS138调试技巧5.1 让加减速更平滑的斜坡实现线性加减速在某些负载下仍会失步尤其是步进电机需要带惯性负载启动时。线性斜坡的加速度是恒定的但步进电机的输出力矩随速度升高而下降理想加减速曲线应该是指数式。这里给一个低成本改进把查表的step_delay值从除法改为查一个预先算好的“指数曲线”表低等级之间差异小高等级之间差异大。比如const uint16_t exp_delay_table[9] { 0, // 占位 8000, 6000, 4500, 3400, 2600, 2000, 1500, 1100, 800 };这样速度从1级升到2级只增加2ms延时但7级升到8级只增加0.3ms实际是越来越快。配合speed_update()里每拍只加一级效果对齐。另一个技巧是启动瞬间强制降低一档。我的做法是在step_motor_start()里先设置current_level为MIN然后让目标等级缓慢爬升。防止用户一开机就把启停和加速同时按下导致突跳。实现上如果检测到target_level - current_level 4则每次只增加2格目标等实际追上后再继续。这是一个简单的S型限制还是用原来的状态机。5.2 74LS138显示乱码的检查清单如果LED显示的速度等级和预期不一致优先检查三根地址线有没有反序。PB12、PB13、PB14分别是A0、A1、A2如果接反了等级1会显示成等级4。我在调试时就会写一个循环让LED依次亮0-7观察顺序是否递增。还有一个隐蔽问题74LS138的G1如果接到STM32的PB15而PB15在JTAG模式里是JTDI复用功能启动时可能被调试器占用。如果下载后PB15默认输出状态不对需要检查重映射设置或者在初始化里单独把GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)关掉JTAG复用。否则PB15无法正常输出高电平。测试LED时可以用逻辑分析仪同时抓取PB12、PB13、PB14对照真值表。没有逻辑分析仪就用万用表测三个引脚电压等级1对应的二进制应该是001也就是PB14为0、PB13为0、PB12为1此时Y1为低LED亮。注意Y0-Y7对应的二进制编码Y1对应的输入是CBA001也就是A01A10A20。如果你的代码里level 0x01赋给A0那等级1恰好是Y1符合直觉。5.3 用示波器验证8拍时序和中断响应最后一个实用技巧是验证输出波形。把示波器探头夹在PA0上地线接GND设置触发为上升沿速度等级设为8拍高频率可以看到占空比接近50%的方波但方波中途会有其他引脚跳变带来的毛刺。更可靠的是同时看PA0和PA1的波形8拍时序下PA0和PA1有时同时为高比如AB状态有时只有PA0为高。如果观察到的波形一直是独立的0.5周期方波说明数组索引没有循环只在两个状态之间来回走。对于外部中断的边沿触发建议用逻辑分析仪看按键按下时PB0的电平毛刺。如果用的是EXTI_Trigger_Rising而按键另一端是VCC且按下接地下拉输入则应该配置成下降沿触发。源码里stm32引脚定义.txt提到按键是接GND所以exti.c中配置的是EXTI_Trigger_Falling。如果你按自己的板子改动这个配置必须和硬件一致否则中断永远不触发。调试时可以在中断回调里加一个短脉冲翻转一个测试IO用示波器测脉冲宽度确认回调的确被触发从而排除GPIO和NVIC配置错误。本文还有配套的精品资源点击获取