文章目录
-
- 一、项目前言
- 二、硬件接线(核心步骤,零基础必看)
-
- [1. 编码器接线(AB相输出)](#1. 编码器接线(AB相输出))
- [2. 电机驱动模块接线](#2. 电机驱动模块接线)
- [3. 共地说明](#3. 共地说明)
- 三、工程创建与基础配置
- [四、定时器初始化代码(tim.c / tim.h)](#四、定时器初始化代码(tim.c / tim.h))
- [五、编码器测速代码(encoder.c / encoder.h)](#五、编码器测速代码(encoder.c / encoder.h))
- [六、电机驱动代码(motor.c / motor.h)](#六、电机驱动代码(motor.c / motor.h))
- [七、PID闭环控制代码(pid.c / pid.h)](#七、PID闭环控制代码(pid.c / pid.h))
- 八、主函数逻辑(main.c)
- 九、系统执行流程图(Mermaid)
- 十、零基础编译与下载步骤
- 十一、参数调节说明
- 十二、常见问题排查
一、项目前言
在嵌入式开发、智能车、机器人等项目中,编码器电机 是最常用的执行部件,而精准测速+闭环控制 是保证电机转速稳定的核心。本教程针对零基础小白,基于最经典的STM32F103C8T6最小系统板,从零开始实现:
- 增量式编码器电机转速测量(单位:转/分钟 rpm)
- 基于PID算法的电机转速闭环控制
- 代码可直接复制编译,硬件接线清晰,步骤详细可落地
硬件准备
- 核心板:STM32F103C8T6最小系统板
- 执行器:带编码器的直流减速电机(13线/11线编码器通用)
- 驱动模块:L298N/DRV8833电机驱动模块
- 电源:12V直流电源(给电机供电)、5V电源(给STM32供电)
- 辅助:杜邦线若干、USB转TTL模块(下载程序)
软件准备
- Keil5 MDK-ARM(STM32F1固件库)
- ST-LINK/USB-TTL下载器
- 串口调试助手
二、硬件接线(核心步骤,零基础必看)
本项目使用定时器编码器模式 读取编码器信号,通用定时器PWM 控制电机转速,定时器中断做测速与PID计算。
1. 编码器接线(AB相输出)
编码器一般有5根线:VCC、GND、A相、B相、Z相(Z相可悬空)
| 编码器引脚 | STM32引脚 | 功能说明 |
|---|---|---|
| VCC | 3.3V | 编码器供电 |
| GND | GND | 共地 |
| A相 | PA0 | 定时器2通道1 |
| B相 | PA1 | 定时器2通道2 |
2. 电机驱动模块接线
以L298N为例,IN1/IN2控制正反转,ENA控制转速
| L298N引脚 | STM32引脚 | 功能说明 |
|---|---|---|
| 12V | 12V电源正极 | 电机供电 |
| GND | STM32 GND + 电源GND | 共地(必须接!) |
| IN1 | PB0 | 电机方向控制1 |
| IN2 | PB1 | 电机方向控制2 |
| ENA | PA6 | 定时器3通道1 PWM输出 |
3. 共地说明
STM32、电机驱动、电源的GND必须连接在一起,否则信号紊乱,无法正常工作!
三、工程创建与基础配置
本教程基于STM32标准库开发,新建工程后,添加标准库文件,然后按照以下步骤创建代码文件。
工程文件结构
我们需要创建5个核心文件:
encoder.c/encoder.h:编码器读取、测速函数motor.c/motor.h:电机驱动、PWM输出函数pid.c/pid.h:PID闭环控制算法tim.c/tim.h:定时器初始化(编码器、PWM、定时中断)main.c:主函数、逻辑调度
四、定时器初始化代码(tim.c / tim.h)
文件名:tim.h
c
#ifndef __TIM_H
#define __TIM_H
#include "stm32f10x.h"
// 定时器初始化函数声明
void TIM2_Encoder_Init(void); // 定时器2 编码器模式初始化
void TIM3_PWM_Init(void); // 定时器3 PWM输出初始化
void TIM4_Init(u16 arr,u16 psc);// 定时器4 中断初始化(10ms中断一次)
int Read_Encoder(void); // 读取编码器计数值
#endif
文件名:tim.c
c
#include "tim.h"
// 定时器2 编码器接口模式初始化
void TIM2_Encoder_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// PA0 PA1 浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 定时器基础配置
TIM_TimeBaseStructure.TIM_Period = 65535; // 自动重装载值
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 编码器模式配置:AB相都计数,四倍频
TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInit(TIM2, &TIM_ICInitStructure);
// 清空计数器
TIM_SetCounter(TIM2, 0);
// 使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
// 读取编码器数值
int Read_Encoder(void)
{
int temp = 0;
temp = (short)TIM_GetCounter(TIM2);
TIM_SetCounter(TIM2, 0); // 读取后清零
return temp;
}
// 定时器3 PWM初始化 PA6
void TIM3_PWM_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// PA6 复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 定时器基础配置:72M/720=100Khz, 自动重装载1000,PWM周期10ms
TIM_TimeBaseStructure.TIM_Period = 999;
TIM_TimeBaseStructure.TIM_Prescaler = 719;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// PWM模式配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
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, ENABLE);
}
// 定时器4 中断初始化 10ms中断一次
void TIM4_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
TIM_TimeBaseStructure.TIM_Period = arr;
TIM_TimeBaseStructure.TIM_Prescaler = psc;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
// 中断配置
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE);
TIM_Cmd(TIM4,ENABLE);
}
五、编码器测速代码(encoder.c / encoder.h)
文件名:encoder.h
c
#ifndef __ENCODER_H
#define __ENCODER_H
#include "stm32f10x.h"
// 外部变量声明
extern int motor_speed; // 电机实时转速 rpm
extern int encoder_count; // 编码器计数值
// 测速函数声明
void Get_Motor_Speed(void);
#endif
文件名:encoder.c
c
#include "encoder.h"
#include "tim.h"
// 全局变量定义
int motor_speed = 0; // 实时转速
int encoder_count = 0; // 编码器计数值
// 电机测速函数(10ms调用一次)
// 编码器线数:13线,减速比:30,四倍频
void Get_Motor_Speed(void)
{
// 读取编码器差值
encoder_count = Read_Encoder();
// 转速计算公式:rpm = 计数值 * 60 / (13*4*30*0.01)
// 13:编码器线数 4:四倍频 30:减速比 0.01:10ms采样时间
motor_speed = encoder_count * 60 / (13 * 4 * 30 * 0.01);
// 负数取绝对值(方向不影响转速大小)
if(motor_speed < 0) motor_speed = -motor_speed;
}
六、电机驱动代码(motor.c / motor.h)
文件名:motor.h
c
#ifndef __MOTOR_H
#define __MOTOR_H
#include "stm32f10x.h"
// 电机控制函数声明
void Motor_GPIO_Init(void); // 电机方向引脚初始化
void Set_Motor_Speed(int speed); // 设置电机转速(0-1000)
void Motor_Forward(void); // 电机正转
void Motor_Backward(void); // 电机反转
void Motor_Stop(void); // 电机制动
#endif
文件名:motor.c
c
#include "motor.h"
#include "tim.h"
// 电机方向控制引脚初始化 PB0 PB1
void Motor_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 初始状态停止
Motor_Stop();
}
// 电机正转
void Motor_Forward(void)
{
GPIO_SetBits(GPIOB, GPIO_Pin_0);
GPIO_ResetBits(GPIOB, GPIO_Pin_1);
}
// 电机反转
void Motor_Backward(void)
{
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
GPIO_SetBits(GPIOB, GPIO_Pin_1);
}
// 电机制动
void Motor_Stop(void)
{
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
GPIO_ResetBits(GPIOB, GPIO_Pin_1);
}
// 设置电机PWM转速 0-1000
void Set_Motor_Speed(int speed)
{
if(speed < 0) speed = 0;
if(speed > 1000) speed = 1000;
TIM_SetCompare1(TIM3, speed);
}
七、PID闭环控制代码(pid.c / pid.h)
文件名:pid.h
c
#ifndef __PID_H
#define __PID_H
#include "stm32f10x.h"
// PID结构体
typedef struct
{
float target; // 目标转速
float actual; // 实际转速
float err; // 当前误差
float last_err; // 上一次误差
float kp; // 比例系数
float ki; // 积分系数
float kd; // 微分系数
float pwm; // 输出PWM值
float integral; // 积分累加值
} PID_TypeDef;
// 外部变量声明
extern PID_TypeDef motor_pid;
// PID函数声明
void PID_Init(void); // PID参数初始化
float PID_Calc(PID_TypeDef *pid, float target, float actual); // PID计算
#endif
文件名:pid.c
c
#include "pid.h"
// 定义PID结构体变量
PID_TypeDef motor_pid;
// PID初始化
void PID_Init(void)
{
motor_pid.target = 0; // 初始目标转速0
motor_pid.actual = 0; // 实际转速
motor_pid.err = 0; // 当前误差
motor_pid.last_err = 0; // 上一次误差
motor_pid.kp = 10.0; // 比例系数(可调节)
motor_pid.ki = 0.5; // 积分系数(可调节)
motor_pid.kd = 0.2; // 微分系数(可调节)
motor_pid.pwm = 0; // 输出PWM
motor_pid.integral = 0; // 积分值
}
// PID增量式算法计算
float PID_Calc(PID_TypeDef *pid, float target, float actual)
{
pid->target = target;
pid->actual = actual;
// 计算误差
pid->err = pid->target - pid->actual;
// 累加积分(限幅防止积分饱和)
pid->integral += pid->err;
if(pid->integral > 1000) pid->integral = 1000;
if(pid->integral < -1000) pid->integral = -1000;
// PID公式计算
pid->pwm = pid->kp * pid->err
+ pid->ki * pid->integral
+ pid->kd * (pid->err - pid->last_err);
// 保存上一次误差
pid->last_err = pid->err;
// PWM限幅 0-1000
if(pid->pwm > 1000) pid->pwm = 1000;
if(pid->pwm < 0) pid->pwm = 0;
return pid->pwm;
}
八、主函数逻辑(main.c)
文件名:main.c
c
#include "stm32f10x.h"
#include "tim.h"
#include "motor.h"
#include "encoder.h"
#include "pid.h"
// 延时函数
void Delay_ms(u16 ms)
{
u16 i,j;
for(i=ms; i>0; i--)
for(j=110; j>0; j--);
}
int main(void)
{
// 模块初始化
TIM2_Encoder_Init(); // 编码器初始化
TIM3_PWM_Init(); // PWM初始化
Motor_GPIO_Init(); // 电机GPIO初始化
PID_Init(); // PID初始化
TIM4_Init(999,719); // 10ms定时中断
// 设置电机正转
Motor_Forward();
while(1)
{
// 主循环可以添加串口打印、逻辑判断等
// 闭环控制在定时器中断中执行
Delay_ms(200);
}
}
// 定时器4中断服务函数 10ms执行一次
void TIM4_IRQHandler(void)
{
if(TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET)
{
// 1. 获取电机实时转速
Get_Motor_Speed();
// 2. PID计算 目标转速:100rpm(可修改)
int pwm_out = PID_Calc(&motor_pid, 100, motor_speed);
// 3. 输出PWM控制电机
Set_Motor_Speed(pwm_out);
TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
}
}
九、系统执行流程图(Mermaid)
系统上电
硬件初始化
编码器定时器初始化
PWM定时器初始化
电机GPIO初始化
PID参数初始化
定时中断初始化
电机设置正转
返回主循环
10ms定时器中断触发
读取编码器计数值
计算电机实时转速
PID算法计算输出PWM
PWM限幅处理
输出PWM控制电机转速
清除中断标志位
十、零基础编译与下载步骤
- 打开Keil5,新建工程,选择STM32F103C8芯片
- 添加STM32标准库核心文件
- 新建上述所有
.c和.h文件,复制代码 - 点击
Build编译工程,确保无报错 - 连接ST-LINK,点击
Download下载程序到开发板 - 给电机驱动模块接上12V电源,电机自动开始闭环调速
十一、参数调节说明
- 编码器参数修改
如果你的电机减速比、编码器线数不同,修改encoder.c中的转速计算公式即可。 - PID参数调节
- 先调节
kp:从小到大,直到电机转速出现小幅震荡 - 再调节
ki:消除静差,让转速稳定在目标值 - 最后调节
kd:抑制超调,让转速响应更平滑
- 先调节
- 目标转速修改
在main.c的中断函数中,修改PID_Calc(&motor_pid, 100, motor_speed)中的100,即可修改目标转速(单位rpm)。
十二、常见问题排查
- 电机不转
- 检查电源是否正常,GND是否共地
- 检查方向引脚、PWM引脚接线是否正确
- 转速读取为0
- 编码器接线错误,A/B相接反
- 编码器模式初始化失败
- 转速不稳定
- PID参数不合适,重新调节
- 电机供电不足,更换大功率电源