☰
STM32编码器+步进电机闭环控制实战
2026/10/21 4:06:06 网站建设 项目流程

简介:本资源是一套基于STM32F103ZET6的电机闭环控制实战项目,面向嵌入式初学者与电机控制进阶开发者,解决步进电机精准驱动与编码器实时反馈同步实现的核心问题,适用于运动控制、智能小车、工业定位等典型场景。压缩包共206个文件,以37个C源码(含stm32f10x_tim.c/rcc.c等标准外设库文件)、38个头文件(h)、35个编译中间文件(o/d/crf)为主干,辅以Keil工程配置(uvprojx/uvoptx/sct)、串口通信调试脚本(bat)及可执行镜像(hex/axf),完整覆盖从定时器PWM驱动步进电机、定时器输入捕获解析增量编码器A/B相、到USART实时上传位置数据的全链路代码实现。资源包大小4.96MB,结构清晰,便于按模块理解底层寄存器配置与中断服务逻辑。目前已有7349人学习下载,提供可直接编译运行的Keil工程、关键外设初始化范例、编码器方向判别与计数更新策略、以及串口数据格式化发送方案,是掌握STM32电机闭环控制原理与工程落地的高实用性参考模板。

1. STM32驱动编码器+步进电机:不是“转起来就行”,而是闭环控制的起点

你手头有一块STM32F103C8T6最小系统板、一个5V两相四线步进电机、一个AB相增量式旋转编码器,接上线、烧进代码,电机真能转——但转得准不准?停得稳不稳?给1000脉冲,它真走完对应角度吗?很多开发者卡在这一步:电机空载时“看起来正常”,一加负载就丢步、抖动、定位漂移,而示波器上A/B相波形明明“有信号”。这不是硬件问题,是开环思维在作祟。这个项目就是为打破这种幻觉而生:它用STM32的定时器编码器接口(TI1/TI2)硬解码AB相,再通过TIM2的PWM+方向引脚驱动步进电机,最后把编码器反馈实时反哺到位置环——整套流程不依赖外部库、不调HAL_Delay,所有中断服务函数(ISR)级响应全部手写寄存器配置。适合正在做智能小车底盘、3D打印Z轴微调、或自动化产线定位模块的嵌入式开发者。如果你的场景需要“给定位置→精确到达→抗扰锁定”,而不是“发个脉冲→听个响”,那这份源码就是你调试闭环的第一块真实砖。


2. 硬件连接与底层驱动:从引脚定义到寄存器级初始化

2.1 编码器接口:为什么必须用TIM3_CH1/TIM3_CH2?

增量式编码器输出A、B两路正交方波,相位差90°。STM32F103的通用定时器(TIM2/TIM3/TIM4)支持编码器接口模式(Encoder Interface Mode),可自动计数并识别旋转方向。本项目固定选用TIM3,原因很实际:

  • TIM3_CH1(PA6)和TIM3_CH2(PA7)物理引脚相邻,布线干扰小;
  • TIM3不被SysTick或FreeRTOS默认占用,避免时基冲突;
  • 其输入滤波寄存器(ETR/CCER)可配置4倍频(即A/B边沿均计数),将100线编码器分辨率提升至400 CPR,显著提高低速定位精度。

提示:不要用PB0/PB1接编码器!这两脚复用为TIM3_CH3/CH4,但F103C8T6的TIM3_CH3无输入捕获功能,无法触发计数。

核心初始化代码如下(encoder.c):

void Encoder_Init(void) { RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // 使能TIM3时钟 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 使能GPIOA时钟 // PA6/PA7 配置为浮空输入(编码器输出为开漏,需外部上拉) GPIOA->CRL &= ~(0xFF << 24); // 清除PA6/PA7配置位 GPIOA->CRL |= (0x04 << 24); // PA6: INPUT_FLOATING GPIOA->CRL |= (0x04 << 28); // PA7: INPUT_FLOATING // TIM3编码器模式配置:非反相,滤波值=8(约1.2us),4倍频 TIM3->SMCR = TIM_SMCR_SMS_3 | // 编码器模式3(TI1和TI2都计数) (0x08 << 8); // IC1F = 0x08, 滤波采样8个时钟周期 TIM3->CCMR1 = TIM_CCMR1_CC1S_0 | // TI1映射到IC1 TIM_CCMR1_CC2S_0 | // TI2映射到IC2 (0x08 << 4) | // IC1F = 0x08(同上) (0x08 << 12); // IC2F = 0x08 TIM3->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E; // 使能IC1/IC2捕获 TIM3->ARR = 0xFFFF; // 自动重装载值设为最大,防溢出 TIM3->CNT = 0; // 初始计数值清零 TIM3->CR1 = TIM_CR1_CEN; // 启动计数 }

参数说明:

  • SMCR_SMS_3是关键——它让TIM3同时监听TI1(A相)和TI2(B相)的上升/下降沿,根据边沿组合自动增减CNT寄存器,无需软件判断方向;
  • IC1F=0x08表示对输入信号进行8周期采样滤波,有效抑制机械抖动引起的误触发(实测可滤除<10kHz噪声);
  • ARR=0xFFFF防止计数器在高速旋转时频繁溢出,后续读取时用有符号16位差值计算位移,避免32位运算开销。

2.2 步进电机驱动:L298N接口与PWM时序约束

本项目采用L298N双H桥驱动两相四线步进电机(如28BYJ-48或42HS系列)。其控制逻辑简单但时序敏感:

  • IN1/IN2控制A相绕组通电方向(决定正反转);
  • IN3/IN4控制B相绕组;
  • ENA/ENB为使能端,接STM32 PWM输出,用于调节电流/速度;
  • 致命约束:L298N内部存在“死区时间”,若IN1与IN2同时为高电平,会触发短路保护锁死。因此必须确保方向信号严格互斥。

项目中将IN1/IN2接PB0/PB1(推挽输出),ENA接PA8(TIM1_CH1,因TIM1支持互补PWM且频率更高),ENB接PA9(TIM1_CH2)。初始化代码(stepper.c)关键段:

void Stepper_Init(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_TIM1EN; // PB0/PB1: 方向控制(推挽,50MHz) GPIOB->CRL &= ~(0xFF << 0); GPIOB->CRL |= (0x02 << 0) | (0x02 << 4); // PB0/PB1: OUTPUT_PP_50MHz // PA8/PA9: PWM输出(TIM1_CH1/CH2) GPIOA->CRH &= ~(0xFF << 0); GPIOA->CRH |= (0x02 << 0) | (0x02 << 4); // PA8/PA9: OUTPUT_PP_50MHz // TIM1基本配置:向上计数,预分频=71(系统时钟72MHz→1MHz),自动重载=999(1kHz PWM) TIM1->PSC = 71; TIM1->ARR = 999; TIM1->CCMR1 = TIM_CCMR1_OC1M_6 | TIM_CCMR1_OC2M_6; // PWM模式1(高有效) TIM1->CCER = TIM_CCER_CC1E | TIM_CCER_CC2E; // 使能CH1/CH2输出 TIM1->BDTR = TIM_BDTR_MOE; // 主输出使能(L298N必需) TIM1->CR1 = TIM_CR1_CEN; } // 设置方向:dir=1正转,dir=-1反转,0停止 void Stepper_SetDir(int8_t dir) { if (dir > 0) { GPIOB->BSRR = GPIO_BSRR_BS0; // PB0=1, PB1=0 → A相正向 GPIOB->BSRR = GPIO_BSRR_BR1; } else if (dir < 0) { GPIOB->BSRR = GPIO_BSRR_BR0; // PB0=0, PB1=1 → A相反向 GPIOB->BSRR = GPIO_BSRR_BS1; } else { GPIOB->BSRR = GPIO_BSRR_BR0 | GPIO_BSRR_BR1; // 全关 } }

逻辑说明:

  • TIM1->BDTR = MOE是L298N驱动的硬性要求——未开启主输出时,即使CCER使能通道,PA8/PA9也无PWM波形;
  • OC1M_6表示PWM模式1(计数器<CCR1时输出高),配合CCER_CC1E实现占空比可调;
  • 方向控制用BSRR寄存器而非ODR,避免读-修改-写竞争(多任务下更安全)。

2.3 电源与信号隔离:为什么你的编码器总读错?

这是新手最常忽略的物理层问题。本项目实测发现:当步进电机启动瞬间,编码器读数跳变±5~10脉冲,根本原因在于共地干扰。解决方案分三级:

  1. 电源分离:电机驱动电源(12V)与MCU电源(5V)完全独立,仅在GND点单点连接;
  2. 信号隔离:编码器A/B相输出经6N137光耦隔离后接入PA6/PA7(本项目源码预留PCB焊盘,未焊接时可直连,但强烈建议加装);
  3. PCB走线:编码器信号线远离电机驱动线(>1cm),并在PA6/PA7旁放置100nF陶瓷电容就近滤波。

注意:若使用28BYJ-48这类减速步进电机,其内部齿轮间隙会导致编码器在换向时产生“回程误差”(backlash),此时单纯提高编码器分辨率无意义,必须在控制算法中加入回程补偿(后文PID章节详述)。


3. 闭环控制算法:从位置环到抗扰动PID调参

3.1 位置环架构:为什么不用速度环?

本项目采用单级位置PID,而非工业常见的“位置环外环+速度环内环”双环结构。原因明确:

  • 步进电机本质是开环执行器,无真实速度反馈(编码器测的是轴端速度,非电机转子速度);
  • 双环会引入额外相位滞后,在100Hz以下低频段易振荡;
  • 实测表明:对定位精度要求≤0.5°、响应时间<200ms的场景,单级PID足够稳定且参数更易收敛。

控制流程图(伪代码):

主循环(10ms周期): 1. 读取TIM3->CNT → 当前编码器计数值(int16_t) 2. 计算位置误差 = 目标位置 - 当前计数值 3. PID计算输出 = Kp*err + Ki*∫err*dt + Kd*(err-err_last)/dt 4. 将输出限幅为[-255, +255] → 映射为PWM占空比(0~100%) 5. 根据误差符号设置Stepper_SetDir()方向 6. 更新TIM1->CCR1 = abs(输出值)

3.2 PID参数手调指南:从“不振荡”到“不超调”

参数整定不是玄学,是分步验证过程。本项目提供一套可复现的调参路径(基于28BYJ-48+100线编码器实测):

步骤KpKiKd现象操作依据
1. 初调Kp0.800电机缓慢蠕动,误差收敛慢Kp过小则响应迟钝,增大至开始轻微振荡(如目标1000,当前在990~1010间摆动)
2. 加Ki0.80.020振荡加剧,但稳态误差归零Ki消除静差,但过大引发积分饱和,观察10秒内是否持续单向爬升
3. 加Kd0.80.020.15振荡抑制,超调<5%, settling time≈120msKd抑制超调,但>0.2时会引入高频噪声(编码器毛刺被放大)

关键技巧:

  • Ki的积分项必须做抗饱和处理:当输出达到限幅值时,暂停积分累加(代码中if (output > 255 || output < -255) integrator = 0;);
  • Kd微分项需微分先行:不直接对误差微分,而是对测量值(编码器计数)微分,避免目标突变时产生巨大冲击;
  • 所有系数存储在const float pid_param[3] = {0.8f, 0.02f, 0.15f};中,方便在线修改(通过串口指令PID Kp 1.2即可重载)。

3.3 抗扰动设计:如何应对突然加载?

真实场景中,电机运行时可能遭遇外力阻挡(如小车撞墙)、负载突变(如3D打印喷嘴接触平台)。此时仅靠PID会大幅超调甚至失步。本项目加入两级防护:

第一级:速度前馈(Velocity Feedforward)
在PID输出基础上,叠加一个与目标速度成正比的前馈量:

// 在主循环中,每10ms计算一次目标速度(单位:脉冲/100ms) int16_t target_speed = (target_pos - last_target_pos) / 10; // 假设目标位置每10ms更新 float feedforward = 0.3f * target_speed; // 前馈增益0.3 output += feedforward;

该设计使电机在加速阶段获得额外驱动力,减少PID调节延迟。

第二级:堵转检测与降速保护
当连续5次采样中,编码器计数值变化量 < 2脉冲,且PWM输出 > 200,判定为堵转:

if (abs(encoder_delta) < 2 && pwm_output > 200) { stall_counter++; if (stall_counter >= 5) { pwm_output *= 0.7; // 立即降速30% stall_counter = 0; } } else { stall_counter = 0; }

实测可使28BYJ-48在堵转时保持扭矩不丢步,松开后自动恢复定位。


4. 常见问题排查:5条血泪经验总结

4.1 现象:编码器计数跳变,每次上电初始值不同

原因:TIM3未在启动时强制清零CNT,且上电瞬间PA6/PA7引脚电平不稳定,被误判为有效边沿。
解决:在Encoder_Init()末尾添加TIM3->CNT = 0;,并在main()中SystemInit()后立即调用Encoder_Init(),避免任何外设初始化顺序干扰。

4.2 现象:电机转动有规律抖动,频率与PWM一致

原因:L298N的ENA/ENB引脚接在同一个定时器通道(如都用TIM1_CH1),导致两相PWM相位完全同步,产生拍频振动。
解决:严格按设计将A相接PA8(TIM1_CH1),B相接PA9(TIM1_CH2),并在TIM1->CCMR1中配置OC1M和OC2M为独立模式,确保两路PWM相位差90°(代码中已实现)。

4.3 现象:PID调节时电机嗡嗡响,但几乎不转

原因:Kp过大导致输出在正负限幅间反复切换,形成“继电器振荡”。
解决:先将Kp降至0.1,确认电机能缓慢跟踪目标;再逐步增加,每次增加后观察10秒,若出现持续振荡立即回退50%。

4.4 现象:串口发送目标位置后,电机无反应

原因:未启用TIM3更新中断(UIE),导致TIM3->SR中的更新标志位无法触发,主循环读不到新计数值。
解决:在Encoder_Init()中添加TIM3->DIER = TIM_DIER_UIE;,并在NVIC中使能TIM3_IRQn,中断服务函数中仅执行TIM3->SR = 0;清除标志(本项目采用查询方式读CNT,故此步可省略,但必须确认TIM3->CR1的URS=0,否则更新事件会误触发)。

4.5 现象:长时间运行后定位偏差累积达数十脉冲

原因:未处理编码器计数器溢出。TIM3_CNT为16位寄存器,满值65535后归零,若主循环未及时读取,会丢失计数。
解决:在主循环中增加溢出检测:

static uint16_t last_cnt = 0; uint16_t curr_cnt = TIM3->CNT; if (curr_cnt < last_cnt && (last_cnt - curr_cnt) > 30000) { // 正向溢出:65535→0,计数增加 total_pos += 65536; } else if (curr_cnt > last_cnt && (curr_cnt - last_cnt) > 30000) { // 反向溢出:0→65535,计数减少 total_pos -= 65536; } total_pos += (int16_t)(curr_cnt - last_cnt); // 强制有符号扩展 last_cnt = curr_cnt;

5. 二次开发改造指南:从定位到轨迹规划

5.1 支持S型加减速:替换梯形速度曲线

当前项目使用最简梯形加减速(匀加速→匀速→匀减速),在启停时冲击大。升级为S型曲线需修改stepper.c中的速度规划模块。核心是将目标位置分解为时间序列点,每10ms计算一个中间位置:

// S型加减速参数(单位:脉冲/100ms) #define MAX_ACC 50 // 最大加速度 #define MAX_VEL 200 // 最大速度 #define JERK 10 // 加加速度(控制S型曲率) typedef struct { int32_t pos; // 当前规划位置 int16_t vel; // 当前规划速度 int16_t acc; // 当前规划加速度 } TrajPoint; TrajPoint traj_gen(int32_t target_pos, int32_t current_pos) { static int32_t remain = 0; static int16_t v = 0, a = 0; remain = target_pos - current_pos; if (abs(remain) < 5) return (TrajPoint){target_pos, 0, 0}; // 三段式S型:加加速段→匀加速段→减加速段... if (v < MAX_VEL) { a += JERK; if (a > MAX_ACC) a = MAX_ACC; v += a; if (v > MAX_VEL) v = MAX_VEL; } else if (v > MAX_VEL) { a -= JERK; v += a; } return (TrajPoint){current_pos + v, v, a}; }

验证方法:用逻辑分析仪抓取PA8(ENA)PWM波形,S型曲线应呈现平滑的sin²形状,而非梯形的直角转折。

5.2 增加CAN总线通信:适配多电机协同

若需控制多个电机(如六足机器人),需将UART升级为CAN。本项目预留了CAN控制器(STM32F103自带bxCAN):

  • 硬件:PA11/PA12接TJA1050收发器;
  • 软件:在can.c中初始化CAN波特率500kbps,ID过滤器设为标准帧0x100~0x1FF;
  • 协议:定义CAN帧格式为ID=0x101, Data[0]=motor_id, Data[1-2]=target_pos_low, Data[3-4]=target_pos_high;
  • 关键修改:将原串口解析函数ParseUartCmd()重构为CanRxCallback(),在CAN1_RX0_IRQHandler中处理。

5.3 低功耗改造:STOP模式下的编码器唤醒

电池供电场景需降低待机电流。F103支持STOP模式(电流≈10μA),但需解决编码器中断唤醒问题:

  • 将编码器A相(PA6)重映射为EXTI线6;
  • 在main()中调用PWR->CR |= PWR_CR_LPDS;进入低功耗;
  • 配置EXTI->IMR |= EXTI_IMR_MR6;使能外部中断6;
  • 进入STOP前执行SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; __WFI();;
  • 唤醒后需重新初始化TIM3(因STOP模式下APB1时钟关闭,TIM3寄存器复位)。

从那以后我每次做电机项目,都会在PCB上提前预留编码器隔离光耦焊盘、L298N散热孔、以及CAN收发器位置——哪怕首版不用,二次开发时至少省去飞线和改板的3天时间。这些不是过度设计,而是把调试周期压缩到最低的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询