
简介面向使用 STM32F103C8T6 驱动 28BYJ-48 步进电机的嵌入式开发者提供完整 Keil 工程模板。程序基于标准外设库实现涵盖 GPIO 脉冲输出、四相八拍步进时序、定时器中断与 PWM 调速、ULN2003 驱动电路适配等关键模块并在步进模式、脉冲频率与旋转方向上做了模块化拆分适合移植到云台、小车、机械臂等小功率精确定位场景。压缩包共 181 个文件以 C 源码、头文件及 Keil 工程配置uvprojx/uvoptx为主同时包含 hex 可烧录文件、axf 调试映像、map 内存映射等编译产物便于核对程序结构与实际烧录运行包体约 4.65MB。模板工程与库函数结构清晰可对照学习脉冲序列生成方式、四相八拍换相顺序、细分驱动原理及中断定时器配置。目前已有 3579 人浏览学习适合需要快速掌握步进电机驱动程序框架并在此基础上进行二次开发或参数调试的嵌入式学习者。1. 为什么 28BYJ-48 和 STM32F103C8T6 是低成本转动方案的黄金搭子步进电机 28BYJ-48 的驱动程序(stm32f103c8t6)这个话题被反复搜到不是因为电机有多高级而是因为它便宜、好买、能转却总有人在第一步接线或第二步时序上卡住。28BYJ-48 是一颗 5V 四相五线减速步进电机自带约 1/64 减速箱扭矩不大但精度和保持力都够做转盘、云台、指针仪表、小型机械臂底座STM32F103C8T6 又是入门级 ARM 板上最常见的芯片GPIO 多、定时器够用C 代码写驱动完全没有障碍。这篇文章要解决的就是三件事先讲清楚它为什么按那个顺序通电才转再给你一套能直接抄到工程里的驱动代码最后把丢步、发热、角度偏差这些玄学问题拆开讲明白。看完你哪怕第一次碰电机也能让它按角度老老实实走。2. 28BYJ-48 接线与拍数先把五根线、四相、八拍一次说清网上很多翻车案例不是程序写错而是线没接对、拍数概念混了。驱动 28BYJ-48 最常用的是配套 ULN2003 驱动板电机五根线插到驱动板上驱动板四个输入引脚再连 STM32F103C8T6 的 GPIO。搞清楚每一根线的作用和每一拍的通电顺序后面调参数才不会像在黑匣子里瞎试。2.1 五根线的颜色与 ULN2003 驱动板怎么接28BYJ-48 出厂带一根五芯线常见颜色排列是红、橙、黄、粉、蓝不同批次颜色会有差异但基本规律不变红色是公共电源线其余四根分别对应四相绕组。用万用表二极管挡量一下红色线对每一根其他线都有相同压降就能确认公共端。电机插到 ULN2003 驱动板的排针上驱动板再把 IN1、IN2、IN3、IN4、VCC、GND 引出来给 MCU 和电源。典型接法如下电机线颜色信号含义接到哪里红VCC 公共端外部 5V 正极或驱动板 VCC 端子橙A 相线圈ULN2003 的 IN1再连到 MCU GPIO黄B 相线圈ULN2003 的 IN2再连到 MCU GPIO粉C 相线圈ULN2003 的 IN3再连到 MCU GPIO蓝D 相线圈ULN2003 的 IN4再连到 MCU GPIO我一般会把 GPIO 选在 PB0、PB1、PB2、PB3这与常见例程的相位表能直接对上方便照抄。ULN2003 内部是一组达林顿管输入高电平时内部开关导通把对应线圈另一端拉到地电流从 5V 通过绕组流向地从而产生磁场。所以 MCU 输出的逻辑高电平就直接推动了达林顿管不需要额外电平转换。注意驱动板的 GND 和 STM32F103C8T6 的 GND 必须共地否则 GPIO 高电平没有参考回路电机只会偶尔抖一下或者完全不转。2.2 四相四拍和四相八拍全步、半步与一圈步数28BYJ-48 是一台四相电机转子的每一步机械角度大约 5.625°减速箱再除以 64输出轴的一拍角度很小。要实现连续转动必须让 A、B、C、D 四相按顺序轮流通电。最常用的两种方式一种是四相四拍也叫全步驱动另一种是四相八拍也就是半步驱动。半步驱动时有些绕组的中间状态也会通电转子每次走半个基本步距转动过程更细腻、振动更小因此我强烈建议默认用八拍。驱动方式通电顺序输出轴一拍角度参考值转动一圈拍数特点四相四拍双相导通AB、BC、CD、DA约 0.176°约 2048 拍程序简单转矩大振动大四相四拍单相导通A、B、C、D约 0.176°约 2048 拍程序简单转矩小定位容易抖动四相八拍半步A、AB、B、BC、C、CD、D、DA约 0.088°约 4096 拍转动平顺推荐日常使用为什么八拍一圈约 4096 拍因为电机本体转一圈需要 360 / 5.625 64 个基本步减速箱按 64:1 算输出轴转一圈就是 64 × 64 4096 个电机基本步。四相八拍每拍走半个基本步所以按拍数算也是 4096 拍对应输出轴一圈。这个数字在项目里经常直接拿来算角度但要注意实际减速齿比会有偏差后面章节我会专门讲怎么标定。2.3 供电与共地为什么电机要用外部 5V 而 GPIO 只要 3.3V28BYJ-48 标称工作电压 5V单相绕组工作电流大约一两百毫安动态换相时峰值更高。如果直接从 STM32F103C8T6 板上取 5VUSB 供电的小开发板很可能会被拉低电压结果是 MCU 莫名复位或者电机转着转着停了。正确做法是给电机单独供 5V可以用一个 USB 充电器或者 5V 开关电源电流能力最好保证在 1A 以上。GPIO 和驱动板输入之前一般不需要加电阻因为 ULN2003 输入端内部有限流电阻STM32F103C8T6 的 GPIO 输出 3.3V 高电平已经能让它可靠导通。共地这条红线一定别省驱动板 GND 要和开发板的 GND 连在一起。我调试时遇到过电机完全不动查了一小时最后发现是 GND 没接。先把电源接好、共地接好再谈程序。3. 在 STM32F103C8T6 上写驱动从 GPIO 初始化到四相八拍正常转起来接线确定后驱动程序的本质就是按顺序翻转四个 GPIO。你不需要了解步进电机内部磁场公式只需要维护一张相位表每过一拍把 GPIO 状态切到下一个。下面这套代码基于标准外设库兼容绝大多数 STM32F103C8T6 工程寄存器版也是同样思路。3.1 最小工程GPIO 配置与八拍相位表先把 PB0 到 PB3 配置成推挽输出速度设 50MHz。步进电机 GPIO 不需要复用功能普通 IO 翻转就够。相位表用 16 位整型数组表示每一位对应一个引脚例如 GPIO_Pin_1 就是 PB1 为高电平。八拍表从只有 A 相通电开始依次加入相邻相形成平滑推进。#include stm32f10x.h #define MOTOR_PORT GPIOB #define MOTOR_PIN_MASK (GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3) // 四相八拍相位表位3对应PB3IN4位0对应PB0IN1 static const uint16_t halfStepTable[8] { GPIO_Pin_0, // A GPIO_Pin_0 | GPIO_Pin_1, // AB GPIO_Pin_1, // B GPIO_Pin_1 | GPIO_Pin_2, // BC GPIO_Pin_2, // C GPIO_Pin_2 | GPIO_Pin_3, // CD GPIO_Pin_3, // D GPIO_Pin_3 | GPIO_Pin_0 // DA }; void Motor_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin MOTOR_PIN_MASK; gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(MOTOR_PORT, gpio); } void Motor_Step(int8_t direction) { static uint8_t phaseIndex 0; if (direction 0) { phaseIndex (phaseIndex 1) 0x07; } else { phaseIndex (phaseIndex 7) 0x07; } // 先清掉四个电机脚再按相位表置位避免影响PB口其他功能 GPIO_ResetBits(MOTOR_PORT, MOTOR_PIN_MASK); GPIO_SetBits(MOTOR_PORT, halfStepTable[phaseIndex]); }这段代码里最关键的是Motor_Step函数用一个静态变量记录当前相位序号正方向加一反方向减一再按 0 到 7 循环。GPIO 操作先整体复位四个引脚再按表置位这样不会把同一时刻多路线圈全导通也不会影响 PB 口上其他引脚。GPIO_Speed_50MHz对步进电机已经足够翻转速度根本不构成瓶颈。用的时候每调用一次Motor_Step(1)就推进半拍反方向调用Motor_Step(-1)。3.2 三种拍频定时方式阻塞延时、SysTick、定时器中断相位表只决定通电顺序真正决定转速的是两拍之间的时间间隔。最简单的测试方式是调用Motor_Step之后让程序睡几毫秒。SysTick 是 Cortex-M3 内核自带的 24 位递减计数器用它做毫秒延时非常合适而且不依赖外设定时器static void DelayMs(uint32_t ms) { uint32_t ticks SystemCoreClock / 1000; SysTick-LOAD ticks - 1; SysTick-VAL 0; SysTick-CTRL SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk; for (uint32_t i 0; i ms; i) { while ((SysTick-CTRL SysTick_CTRL_COUNTFLAG_Msk) 0) { } } SysTick-CTRL 0; }SystemCoreClock一般被系统初始化设置成 72MHz所以ticks算出来是 72000即计时 1ms。每拍延时 5ms对应约 200Hz 拍频是 28BYJ-48 比较从容的转速如果改成 2ms速度接近 500Hz空载通常还能跑但负载稍大就可能失步。阻塞延时的优点是代码简单缺点是一旦开始转整个 CPU 都在等延时。对只需要匀速转动的项目够用要同时处理按键、显示屏就会明显卡顿。更好的方案是定时器中断驱动。这里用 TIM2预分频器把 72MHz 降到 10kHz自动重装载值设为 10就得到 1kHz 的更新中断频率也就是中断函数每秒被调用 1000 次void Motor_TimerInit(void) { TIM_TimeBaseInitTypeDef timer; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); timer.TIM_Prescaler 7200 - 1; // 72MHz / 7200 10kHz timer.TIM_CounterMode TIM_CounterMode_Up; timer.TIM_Period 10 - 1; // 10kHz / 10 1kHz timer.TIM_ClockDivision 0; TIM_TimeBaseInit(TIM2, timer); TIM_Cmd(TIM2, DISABLE); } void Motor_TimerStart(void) { TIM_Cmd(TIM2, ENABLE); TIM_ClearFlag(TIM2, TIM_FLAG_Update); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_EnableIRQ(TIM2_IRQn); } void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if (g_stepsRemaining 0) { Motor_Step(g_stepDirection); g_stepsRemaining--; } else if (g_stepsRemaining 0) { // 到位后可以选择保持当前相位或全部断电 } } }g_stepsRemaining是剩余步数由主程序在需要转动前赋值。每次中断只推进一步推完自动停。这种做法的好处是电机转动期间主循环照常跑按键扫描、 OLED 刷新只是到了移动结束再处理结果。想让电机加速、减速只需要在一个更长周期里动态改变定时器重装载值后面我会讲具体做法。3.3 方向参数与动作封装把方向、延时、目标角度封装成一个函数调用端就不用关心拍数了。注意浮点运算在 MCU 上较慢只在计算目标步数时用一次循环内不要出现浮点运算。void Motor_Rotate(float angle, uint8_t direction, uint16_t delayMs) { int32_t totalSteps; // 以八拍、约4096拍/圈为基准传入角度换算成拍数 totalSteps (int32_t)(angle / 360.0f * 4096.0f 0.5f); for (int32_t i 0; i totalSteps; i) { Motor_Step(direction); DelayMs(delayMs); } }angle是输出轴要转的角度direction传 1 或 -1delayMs决定速度。四舍五入用0.5f避免了角度和拍数转换时累计截断误差。比如要转 90 度算出来 1024 拍如果直接整型截断每 90 度会少半步看起来不多转 10 次后就能看出指针回不到原点。这个函数只适合低速验证正式项目建议用定时器中断版本因为阻塞延时期间按键和看门狗都会受影响。4. 把每圈步数和速度算准标定、防丢步与步进频率转动方向对了、电机能转了下一步要解决的是“转得多准、转得多快”。28BYJ-48 的参数表会写 5.625°也写减速比 1/64但实际产品的一致性没那么理想直接拿理论值算角度累计几十圈就会明显偏。这里提供一套标定方法和几个必调参数。4.1 每圈步数从 5.625°/64 推导再做一个实际标定按电机本体 5.625°、减速比 64:1 计算输出轴转一圈需要 4096 个八拍拍数。很多开源库里写 2048那是把四相四拍全步驱动当作基准八拍模式下一圈拍数翻倍。我一般先按 4096 写进程序再用纸刻度盘实测修正。标定方法很简单让电机走上千拍量输出轴实际转角。#define TEST_PHASE_NUMBER 1000 void Motor_CalibrationTest(void) { Motor_Init(); DelayMs(100); for (uint32_t i 0; i TEST_PHASE_NUMBER; i) { Motor_Step(1); DelayMs(5); } }运行这段代码之前在电机轴上粘一根细铁丝作为指针在正前方贴一张画了 0 到 180 度的纸刻度盘。记下指针起始位置运行完Motor_CalibrationTest后再量终止角度。如果测得角度是 88° 左右说明与标称接近假设实测是 88.2°那每拍实际角度就是 88.2 / 1000 0.0882°转一圈拍数就是 360 / 0.0882 ≈ 4082 拍。把算出来的拍数替换掉代码里的 4096之后的角度定位就会明显更准。注意测量时电机轴要加一个轻负载比如一根小纸板指针这样转速慢、读数稳定也更接近真实工作状态。4.2 用多快的频率驱动它一拍到底多久步进电机的转速不是越快越好28BYJ-48 内部减速齿比大转子转得快但输出轴只能慢慢走。常用拍频范围如下供新手设定初始值一拍间隔近似拍频输出轴转速八拍适用场景8ms125Hz约 0.03 圈/s定位演示、教学几乎不会失步5ms200Hz约 0.05 圈/s默认调试值空载可靠3ms约 333Hz约 0.08 圈/s实测确认扭矩足够后再用2ms500Hz约 0.12 圈/s空载或轻负载需防失步1ms 以下大于 1kHz面板转得快不建议很容易丢步失步的表现是电机发出急促的咔咔声输出轴停住或者比预期少走一段同时电流明显增大线圈发烫。遇到失步先不要加电机电流那是高级电机才做的事这颗电机的核心解决方法是把一拍间隔拉长回到 5ms 看看是否恢复。记住一个原则28BYJ-48 追求的是低速大扭矩把高速交给别的电机。4.3 加减速的简单实现延迟数组与分段变速电机从静止直接进入 500Hz 拍频就像车子一脚油门上高速必然打滑。常见做法是初始化时用较长的间隔每走几十拍缩短一点接近目标再拉长。用数组保存一段延迟曲线取第几个档位由总拍数决定// 加速段和减速段共用一个延迟表数值是每拍延时(ms) static const uint16_t rampTable[9] { 40, 30, 20, 15, 12, 10, 8, 6, 5 }; void Motor_RotateRamp(float angle, int8_t dir) { int32_t total (int32_t)(angle / 360.0f * 4096.0f 0.5f); int32_t step; int32_t accelSteps total / 2; // 前半段加速后半段减速 for (step 0; step total; step) { uint32_t rampIndex step * 8 / accelSteps; uint32_t delayMs; if (rampIndex 8) rampIndex 8; if (step accelSteps) { delayMs rampTable[rampIndex]; } else { // 回程减速越接近目标越慢 int32_t remainSteps total - step; uint32_t decIndex (total - remainSteps) * 8 / accelSteps; if (decIndex 8) decIndex 8; delayMs rampTable[8 - decIndex] rampTable[rampIndex] ? rampTable[8 - decIndex] : rampTable[rampIndex]; } Motor_Step(dir); DelayMs(delayMs); } }这段代码不是最优写法但足够说明加减速的核心想法拍频从低频开始逐步升高快到位再降下来。要做得更细可以把延迟表拉长到 32 档让加减速更平滑。更重要的是不要过度优化计算精度步进电机是机械旋转系统真实转动效果受负载影响比理论精度大得多调表后实测一遍才是准的。5. 28BYJ-48 STM32F103C8T6 的避坑清单五条现场翻车记录驱动例程能在网上找到一堆真正劝退新手的是各种不按套路出牌的现象。这一章把我踩过的坑和替别人排查过的问题整理成清单每条都按现象、原因、解决顺序来写。5.1 电机嗡嗡响但不转摸上去还很烫现象是在通电状态下电机发出持续嗡嗡声输出轴偶尔抖一点停留几秒后外壳明显发热。原因通常是两个一是公共端正负极接反四相绕组形成对地短路二是供电电压不够但电流过大线圈被“焊住”在某一相上无法换相。先量 ULN2003 驱动板 VCC 和 GND 之间电压确认有 5V再断电检查电机五芯插头是否插反红色线是否接到 VCC 端子。若都正常让电机停下来冷却后用手拨动输出轴如果能自由旋转则不是机械卡死继续查相位表。从经验看这种故障九成出在电源或接线程序没问题。5.2 低速正常一加快就咔咔响或角度跑偏现象是 5ms 一拍时电机转动顺畅改成 2ms 后开始失步角度一次比一次偏。原因是拍频超过了电机能跟上的极限转子未到位就切换相位产生抖动。解决方法是把一拍间隔调回 3ms 以上并检查 MCU 端是否在每次调用之间还做了其他耗时操作比如打印日志、刷新显示屏。如果必须跑快只能配合加减速曲线从低速爬升快到目标前再减速。任何 28BYJ-48 在中低负载下都不要追求满载高速它定位的强项在低速走位。5.3 每次转固定角度都有稳定偏差偏差方向一致现象是让电机转 100 圈每 10 圈检查一次越到后面差得越多但每次偏差几乎相同不是随机抖动。原因是理论每圈拍数 4096 与实际减速比不一致减速箱实际齿比通常在 63.68 左右不是正好 64长时间累积就会产生可观察误差。解决方法是做一次标定测量把stepsPerRevolution改成实测值。这个偏差不是程序逻辑问题也不是电源问题改了步数常数就能消除。5.4 上电瞬间电机会自己跳一步现象是一上电、还没调用任何步进函数电机轴轻微动一下有时还伴随一声短鸣。原因是四个 GPIO 在上电复位期间状态不确定可能呈现出某些引脚下拉、某些输出高电平导致某相通电。解决方法有两个在初始化函数里首先把四个电机引脚全部配置为推挽输出并输出低电平如果项目有硬件条件给 ULN2003 驱动的输入端接一组下拉电阻让未初始化阶段始终为低电平。这个现象不影响后续转动但会给带初始定位的项目留下位置误差。5.5 照着例程接反而反转方向不对或卡死现象是移植别人的代码后正向要求电机却反转或者转到某一步突然卡住。原因是相位表顺序与实际接线对应不上PB0 到 PB3 不是按 IN1、IN2、IN3、IN4 顺序连接。解决方法是先确认接线再看相位表。如果接反了某两根线把这两对应地交换或者保持接线不变仅把相位表顺序镜像反转。电机卡死的那一步通常是相邻两相同时导通但顺序跳错检查数组里每个元素的位组合是否有从0x09到0x01这类跳跃就会发现问题。6. 让电机转得更体面定时器中断、S 曲线式启停与一套验证习惯前面代码可以跑通但要达到“能交付”的程度通常还要做两件事把转动从主循环里解放出来以及给每台电机留一套验收方法。定时器中断驱动步进电机是把步进动作变成一个后台任务。主程序只需要设置目标步数和方向剩下的由 TIM 更新中断完成。中断函数里建议每次都重新装载计数值因为这样可以实现动态调速加速时逐渐减小重装载值减速时逐渐增大重装载值。这比在主循环里改延时更及时也不会因为正在转动的过程中刷新显示屏而卡一拍导致丢步。注意中断里只做步进和计数不要做角度换算、浮点运算和日志打印这些动作延迟不可控。验收方法我习惯每次都做三步第一步空载跑 1000 拍用量角器量实际转角算出每圈实际拍数第二步带实际负载跑 1 圈重复 10 次记录每次停止位置偏差应控制在半拍以内第三步做一个往返测试让电机正转 10 圈再反转 10 圈检查是否回到起点。来回能回位才说明没有丢步累积。这个方法不挑负载花十分钟就能给电机是不是正常工作下一个明确结论。最后说一个我自己的习惯拿到 28BYJ-48 先不写程序先手动按相位顺序用杜邦线给四相轮流接 5V听声音、感受换相节拍。这样能立刻知道电机本身有没有问题再上代码排错范围就会小很多。希望帮到你。本文还有配套的精品资源点击获取