简介:这份资源面向嵌入式开发初学者与电机控制方向的工程师,提供基于STM32F103C8T6微控制器与A4988驱动芯片控制2相4线42步进电机的完整工程源码。项目通过定时器输出脉冲精确控制步进频率,利用外部中断响应按键实现启停与换向,并借助串口收发与上位机通信,完成速度、方向等参数的远程调节,可应用于自动化设备、3D打印机与机器人等定位场景。压缩包共203个文件,约6.81MB,包含34个c源文件与35个h头文件构成的核心驱动代码、若干o与crf编译中间文件、2个hex可执行文件,以及uvprojx、uvoptx等Keil工程配置文件和bat批处理脚本,目录结构清晰,便于直接编译与二次开发。目前已有1339人学习下载,适合希望快速掌握步进电机驱动与串口远程控制方案的读者参考借鉴。
1. 从一块 C8T6 和一颗 A4988 说起:这套 42 步进电机驱动源码到底能干什么
如果你手上正好有一块 STM32F103C8T6 最小系统板、一颗 A4988 驱动模块、一个 42 两相四线步进电机,却卡在“线怎么接、脉冲怎么发、串口怎么调”这三件事上,那这套源码就是冲着你来的。它把 C8T6 的定时器 PWM 输出、GPIO 方向控制、A4988 的细分拨码逻辑,以及串口收发指令解析全部串成了一条完整链路——上电后电机能按你发的串口命令正反转、调速、定步数走。适合刚接触 STM32 又想快速看到电机转起来的新手,也适合拿它当运动控制底板做二次开发的熟手。我拆过不少步进驱动的小工程,这套代码最省心的地方在于:它没把东西藏进库里,所有寄存器配置和中断逻辑都摊在.c文件里,改一个参数就能看到电机反应,调试起来不玄学。
2. 硬件接线与工程结构:先让电机安全转起来
2.1 接线表与拨码开关,接错一步就烧驱动
A4988 这颗驱动模块便宜好用,但它对电源和接线顺序很敏感。我见过太多人先把 12V 电机电源接上、再插驱动、最后接 C8T6,结果一上电 A4988 直接冒烟。正确顺序是:先接好 C8T6 与 A4988 的逻辑线,再接电机四线,最后才上电机电源。下面这张表是我按源码里motor.h的宏定义整理的接线关系,你照着插就行。
| C8T6 引脚 | A4988 引脚 | 作用 | 源码宏定义 |
|---|---|---|---|
| PA5 | STEP | 脉冲输入,每个上升沿走一步 | STEP_PIN |
| PA6 | DIR | 方向电平,高/低对应正反转 | DIR_PIN |
| PA7 | ENABLE | 使能,低电平有效 | EN_PIN |
| 3V3 | VDD | 逻辑供电 | — |
| GND | GND | 共地,必须接 | — |
| 12V 电源 | VMOT | 电机供电,并联 100uF 电解电容 | — |
42 两相四线电机的四根线,用万用表蜂鸣档量一下:相通的两根是一组线圈,A+、A- 接 A4988 的 1A、1B,B+、B- 接 2A、2B。接反了电机不会烧,但会抖动不转,这是最常见的“翻车”现场。A4988 上的三个拨码开关决定细分:全低是整步,全高是 1/16 细分。源码默认按 1/16 细分计算步数,也就是电机转一圈需要200 * 16 = 3200个脉冲。你要改细分,记得同步改STEPS_PER_REV这个宏,否则串口发“转一圈”实际转的角度对不上。
2.2 目录结构:五个文件撑起整个工程
这套源码没有用 HAL 库那套臃肿的封装,走的是标准外设库的路子,工程目录干净得让人舒服。核心文件就五个:
Project/ ├── USER/ │ ├── main.c // 主循环,串口命令解析入口 │ ├── stm32f10x_it.c // 中断服务,串口接收中断在这 ├── HARDWARE/ │ ├── motor.c/.h // 定时器PWM、GPIO方向、步数控制 │ ├── usart.c/.h // 串口初始化与收发 ├── SYSTEM/ │ ├── delay.c/.h // 滴答定时器延时motor.c是心脏,里面用 TIM3 的通道 1 输出 PWM 作为 STEP 脉冲,改ARR寄存器就是改脉冲频率,也就是改转速。usart.c把串口 1 配成 115200、8 位、无校验,接收中断里逐字节存进缓冲区,主循环里再解析。这种“中断收、主循环解”的写法虽然朴素,但胜在稳定,不会因为解析耗时丢字节。你拿到工程后,先别急着改代码,用 Keil 打开编译一遍,确认 0 错误 0 警告,再烧录。烧录后如果电机没反应,先量 PA5 有没有脉冲输出——没有脉冲,后面全白搭。
3. 核心模块拆解:脉冲怎么发、串口怎么收
3.1 定时器 PWM 发脉冲,频率就是转速
步进电机的转速只取决于脉冲频率,跟电压电流没关系(那是力矩的事)。源码里用 TIM3 产生 PWM,每个周期输出一个脉冲,频率计算公式是72MHz / ((ARR+1)*(PSC+1))。我一般把预分频PSC固定成 71,这样计数时钟就是 1MHz,改ARR直接对应微秒级周期,心算方便。下面这段是motor.c里初始化定时器的关键代码:
// TIM3 初始化,PA5 复用推挽输出 void Motor_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // PA5 作为 TIM3_CH1 复用输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 方向脚 PA6、使能脚 PA7 普通推挽 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 999; // ARR,决定脉冲周期 TIM_TimeBaseStructure.TIM_Prescaler = 71; // PSC,1MHz 计数 TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 500; // 占空比 50% TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, DISABLE); // 先不启动,等命令 }这段代码的逻辑很直白:先把 PA5 配成复用推挽,让 TIM3 的通道 1 接管它;然后设置ARR=999、PSC=71,算下来脉冲频率是1MHz / 1000 = 1kHz,也就是每秒 1000 步。按 1/16 细分、3200 步一圈算,转速约 0.31 转/秒。想提速就把ARR改小,比如改成 499,频率翻倍到 2kHz。注意TIM_Cmd(TIM3, DISABLE)这行——初始化完先不启动,等串口收到命令再开,避免上电电机就乱转。改转速时只动ARR,别动PSC,因为PSC一变,整个时间基准都变了,容易把步数计算搞乱。
3.2 串口收发:中断收字节,主循环解命令
串口这块源码用的是最经典的“接收中断 + 空闲判断”方案。每收到一个字节进一次中断,存进USART_RX_BUF,同时清零计数器;主循环里检查计数器是否超过 10ms 没变,没变就认为一帧收完了。命令格式设计成方向+速度+步数,比如发F1003200表示正转、1kHz、走 3200 步(一圈)。下面看接收中断和解析的核心代码:
// 串口1接收中断,逐字节存入缓冲区 void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { u8 res = USART_ReceiveData(USART1); if(USART_RX_CNT < USART_REC_LEN) { USART_RX_BUF[USART_RX_CNT] = res; USART_RX_CNT++; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } // 主循环里的命令解析 void Command_Parse(void) { if(USART_RX_CNT == 0) return; // 简单判断首字符决定方向 if(USART_RX_BUF[0] == 'F') { GPIO_ResetBits(GPIOA, GPIO_Pin_6); // DIR 低,正转 } else if(USART_RX_BUF[0] == 'R') { GPIO_SetBits(GPIOA, GPIO_Pin_6); // DIR 高,反转 } // 解析速度值,改 ARR 寄存器 u16 speed = (USART_RX_BUF[1]-'0')*100 + (USART_RX_BUF[2]-'0')*10 + (USART_RX_BUF[3]-'0'); TIM3->ARR = 1000000 / speed - 1; // 频率转 ARR // 解析步数,启动定时器并计数 target_steps = atoi((char*)&USART_RX_BUF[4]); current_steps = 0; TIM_Cmd(TIM3, ENABLE); USART_RX_CNT = 0; // 清空缓冲,准备下一帧 }中断里只做一件事:存字节、计数。解析放在主循环,避免在中断里做浮点运算和字符串处理,这是嵌入式开发的铁律。TIM3->ARR = 1000000 / speed - 1这行是核心换算:因为PSC=71时计数时钟是 1MHz,要得到speedHz 的频率,周期就是1000000/speed微秒,减 1 是因为 ARR 从 0 开始计。步数控制靠另一个定时器中断或者主循环里判断current_steps >= target_steps就关定时器。源码里用的是 TIM2 做步数计数,每发一个脉冲进一次中断,current_steps++,到了就TIM_Cmd(TIM3, DISABLE)。这里有个细节:TIM2 的计数频率必须和 TIM3 的脉冲频率一致,否则步数会数错。我一般把 TIM2 也配成 1MHz 计数、ARR 跟 TIM3 同步,这样每来一个脉冲 TIM2 就溢出一次,稳得很。
4. 避坑与常见问题:电机不转、串口乱码、驱动发烫
4.1 电机嗡嗡响但不转,先查线圈分组
现象:上电后电机有电流、有嗡嗡声,但轴不转或者只抖动。原因九成是两相四线接错了组——把同一相的两根线接到了 A4988 的不同输出端。解决:断电,用万用表蜂鸣档量电机四根线,相通的两根是一组,分别接 1A/1B 和 2A/2B。如果量出来四根两两都通,那电机是坏的,换一个。
4.2 串口发命令没反应,检查波特率和共地
现象:串口助手显示发送成功,但电机纹丝不动。原因通常是 C8T6 和 USB 转串口模块没共地,或者波特率不是 115200。源码里usart.c初始化写死了 115200,你串口助手也得设成一样。另外 PA9/PA10 别接反,PA9 是 TX 接模块 RX,PA10 是 RX 接模块 TX。解决:先短接模块的 TX 和 RX 做自收发测试,确认模块本身没问题,再查接线。
4.3 A4988 发烫严重,调参考电压或加散热片
现象:电机能转,但 A4988 芯片烫得手摸不上去,几分钟后丢步。原因:A4988 的电流限制没调,默认可能超过电机额定电流。解决:用一字螺丝刀拧模块上的电位器,逆时针调小电流。测量方法:万用表接VREF和 GND,42 步进电机一般额定 1.5A 左右,对应VREF约 1.2V(公式I = VREF / (8 * Rs),Rs 通常 0.1Ω)。调好后贴个小散热片,别裸奔。
4.4 步数走不准,细分和步数宏没对齐
现象:发“走一圈”命令,电机实际转了半圈或两圈。原因:A4988 拨码开关的细分设置和源码里STEPS_PER_REV宏不一致。比如拨码设了 1/8 细分,一圈需要 1600 步,但源码按 1/16 算成 3200,自然多转一倍。解决:先确认拨码状态,再改motor.h里的STEPS_PER_REV,两者必须一致。改完重新编译烧录。
4.5 上电电机就转,使能脚逻辑搞反了
现象:一上电电机就自己转,串口还没发命令。原因:A4988 的 ENABLE 是低电平有效,源码里EN_PIN初始化后如果默认输出低,驱动就使能了。解决:在Motor_Init()里把 PA7 初始电平设成高,或者上电后先GPIO_SetBits(GPIOA, GPIO_Pin_7),等收到命令再拉低使能。这个坑我踩过,半夜电机突然转起来,吓一跳。
5. 二次开发:把定步数改成连续调速,再加个加减速曲线
这套源码跑通之后,最值得改的地方是运动控制逻辑。原版是“发命令、走固定步数、停”,实际项目里更常用的是连续调速和加减速。我一般会先加一个速度渐变:在 TIM2 的步数中断里,每走 100 步就把TIM3->ARR减一点,让频率慢慢升上去,避免启动瞬间丢步。下面这段是加减速的简化实现,你可以直接塞进stm32f10x_it.c的 TIM2 中断里:
// TIM2 中断,每发一个脉冲进一次 void TIM2_IRQHandler(void) { if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { current_steps++; // 每 100 步调整一次速度,实现梯形加减速 if(current_steps % 100 == 0) { if(current_steps < target_steps / 2) { // 加速段:ARR 减小,频率升高 if(TIM3->ARR > MIN_ARR) TIM3->ARR -= 10; } else { // 减速段:ARR 增大,频率降低 if(TIM3->ARR < MAX_ARR) TIM3->ARR += 10; } } if(current_steps >= target_steps) { TIM_Cmd(TIM3, DISABLE); // 走完停脉冲 TIM_Cmd(TIM2, DISABLE); current_steps = 0; } TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }这段逻辑的关键参数是MIN_ARR和MAX_ARR,分别对应最高速和最低速的周期值。MIN_ARR别设太小,A4988 最高支持约 200kHz,但 42 电机在 1/16 细分下超过 10kHz 就容易丢步,我一般把MIN_ARR设在 100 左右,对应 10kHz。加减速的步长10也可以调,调大了加速猛但容易丢步,调小了加速平缓但耗时。改完记得把target_steps的解析逻辑也改一下,支持“连续转”模式——比如收到C开头命令就不设目标步数,只开定时器,收到S才停。
验证方法很简单:拿手机慢动作录像拍电机轴,看启动和停止时有没有明显的“咯噔”丢步声。如果有,把加速步长调小、MIN_ARR调大,直到声音平滑。我习惯在改完运动参数后,先空载跑 100 圈,再带上负载跑 50 圈,确认不丢步才算过。从那以后我每次调步进电机,都强制先空载验证加减速曲线,再上负载——这个习惯帮我省了至少三次拆机重焊的麻烦。希望这套源码和上面的改造思路,能帮你把 42 步进电机稳稳转起来。
本文还有配套的精品资源,点击获取