目录
- 前言
- 一、系统总览:五个模块各管什么
- 二、电源与电源模块:别让电机把 MCU 拖垮
- 三、L298N 驱动模块:H 桥的真值表要刻进脑子
- 四、STM32 控制模块:TIM 资源分配
- 五、电机模块:编码器原理与接线
- 六、CubeMX 配置(TIM3 PWM + TIM4 编码器)
- 七、HAL 库代码
- 八、标准库代码(STM32F103)
- 九、开环 → 闭环:开发流程的四个阶段
- 十、避坑清单(实战干货)
- 十一、文章收尾:三条可执行结论
前言
本文是一篇面向实战的 STM32 电机控制入门教程,主线是把「电源 → L298N → STM32 → 编码电机」这条链路完整串起来:用 L298N 做功率放大与方向控制,用 STM32 定时器输出 PWM 调速,用编码器接口做硬件计数测速,最后在代码层把「开环 PWM」升级到「速度闭环 PID」。
全文按工程落地顺序展开,从系统总览、双电源设计、H 桥控制逻辑,到 TIM 资源分配、编码器原理、CubeMX 配置,再到 HAL 库与标准库两套可运行代码,最后给出开环到闭环的四阶段开发流程和避坑清单。每一节都对应一个具体的工程动作,语言力求简短,读者可以边看边动手。
阅读本文前,建议先准备好 STM32F103 开发板、L298N 模块、带霍尔编码器的有刷直流电机以及 12V 电源,并安装好 STM32CubeMX 与 Keil(或 STM32CubeIDE)。如果你已经跑通过基础的点灯和 GPIO 例程,上手会更快。
一、系统总览:五个模块各管什么
| 电源 | 12V 电池 / 适配器 | 给电机提供功率电 | 电流 ≥ 电机额定电流 2 倍 |
| 电源模块 | LDO / DC-DC(5V/3.3V) | 给 STM32 和编码器逻辑供电 | 与电机电源共地,建议分开布线 |
| L298N 模块 | L298N 双 H 桥 | 功率放大 + 方向切换 + PWM 调速 | 逻辑电源 5V,驱动电源 12V,必须共地 |
| STM32 控制模块 | STM32F1/F4 等 | 产生 PWM、读编码器、跑 PID | TIM 资源分配:一路 PWM 定时器 + 一路编码器定时器 |
| 电机模块 | 有刷直流电机 + 霍尔编码器 | 执行机构 + 位置/速度反馈 | A/B 相正交输出,Z 相零位(若有) |
核心原理一句话:PWM 占空比决定电机两端的平均电压(即转速大小),IN1/IN2 电平组合决定电流方向(即正反转),编码器 A/B 相硬件计数反映实际转过的角度/速度,三者构成"指令—执行—反馈"闭环。
二、电源与电源模块:别让电机把 MCU 拖垮
1. 为什么要"双电源"
电机启动瞬间的浪涌电流会拉低电源轨,若与 STM32 共用同一路 12V,会导致 MCU 复位甚至损坏。推荐方案:
- 电机电源:12V 电池或适配器,单独给 L298N 的 VM 端供电
- 逻辑电源:USB 5V 或 DC-DC 降压到 3.3V,给 STM32 和编码器供电
- 共地:两路电源的 GND 必须连在一起,否则信号无参考
2. L298N 电源引脚
- VS(VM):电机驱动电源,接 12V
- VSS(+5V):逻辑电源,接 5V(部分模块可由板载 5V 输出反供 STM32)
- GND:与 STM32 GND、12V 负极连通
罗姆对直流有刷电机 PWM 驱动的解释很到位:PWM 导通时电压施加到电机两端,关断时电机两端被短路实现电流再生,故电感电流纹波由占空比和周期决定。这正是我们用 L298N 做调速的物理基础。
三、L298N 驱动模块:H 桥的真值表要刻进脑子
1. H 桥控制逻辑(单路电机)
| H | L | 正转 |
| L | H | 反转 |
| H | H | 刹车(短路制动) |
| L | L | 停止(惯性滑行) |
| ENA=PWM | — | 调速(ENA 为高时 H 桥工作) |
ENA 接 PWM 信号,占空比越大,电机两端平均电压越高,转速越快。
2. 典型接线(以 STM32F103C8T6 为例)
| PA6 (TIM3_CH1) | ENA | PWM 调速 |
| PB12 | IN1 | 方向控制 A |
| PB13 | IN2 | 方向控制 B |
| PB14 (TIM4_CH3) | ENB | 第二路 PWM(若驱双电机) |
| PB15 | IN3 | 第二路方向 A |
| PA8 (TIM1_CH1) | IN4 | 第二路方向 B |
| GND | GND | 共地 |
3. 输出端
- OUT1、OUT2 → 电机两极
- L298N 内部续流二极管已集成在模块上,无需外接
四、STM32 控制模块:TIM 资源分配
关键原则:用两个不同的定时器——一个做 PWM 输出,一个做编码器接口。
推荐分配(F103 为例):
- TIM3:PWM 输出(通道 1 → ENA),72MHz/72/1000 = 1kHz,或 72MHz/72/100 = 10kHz
- TIM4:编码器模式(CH3/PB8 = A 相,CH4/PB9 = B 相),4× 倍频
- 方向控制:PB12/PB13 普通 GPIO 推挽输出
- PID 周期:TIM2 或 SysTick 产生 10ms 中断,用于速度环计算
五、电机模块:编码器原理与接线
1. 正交编码原理
编码器码盘随电机轴旋转,A、B 两相输出 90° 错相方波:
- 正转:A 超前 B 90°
- 反转:B 超前 A 90°
STM32 定时器编码器接口在 TI1 和 TI2 双边沿都计数(4× 倍频),硬件自动识别方向并增减 CNT,零 CPU 开销。
2. 接线
| VCC | 5V | 编码器供电 |
| GND | GND | 共地 |
| A 相 | PB8 (TIM4_CH3) | 编码器输入 1 |
| B 相 | PB9 (TIM4_CH4) | 编码器输入 2 |
| Z 相(若有) | 任意外部中断引脚 | 零位校准 |
3. 关键参数
- PPR(每转脉冲数):如 15 线霍尔编码器 × 34:1 减速比 = 510 PPR(电机轴)
- CPR(每转计数数) = 4 × PPR = 2040(4× 倍频后)
- 转速换算:RPM = Δcount / CPR / Δt × 60
六、CubeMX 配置(TIM3 PWM + TIM4 编码器)
TIM3 配置(PWM 输出)
| Clock Source | Internal Clock |
| Channel1 | PWM Generation CH1 |
| Prescaler | 71(72MHz/72=1MHz) |
| Counter Period | 999(1kHz PWM) |
| Pulse | 0(初始占空比 0) |
| AutoReload Preload | Enable |
TIM4 配置(编码器接口)
| Combined Channels | Encoder Mode |
| Encoder Mode | TI1 and TI2(4× 倍频) |
| IC1/IC2 Polarity | Rising Edge |
| IC1/IC2 Filter | 6(机械编码器抗抖) |
| Counter Period | 65535 |
| NVIC | TIM4 global interrupt → Enabled(用于溢出扩展) |
GPIO 配置
- PB12、PB13:Output Push-Pull,初始 Low
七、HAL 库代码
1. 初始化与启动
// main.c 中 /* USER CODE BEGIN 2 */ 区域
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 启动 PWM
HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL); // 启动编码器
HAL_TIM_Base_Start_IT(&htim4); // 启动溢出中断(用于 32 位扩展)
// 方向控制宏
#define MOTOR_FORWARD {HAL_GPIO_WritePin(GPIOB, GPIO_Pin_12, GPIO_PIN_SET); \\
HAL_GPIO_WritePin(GPIOB, GPIO_Pin_13, GPIO_PIN_RESET);}
#define MOTOR_BACKWARD {HAL_GPIO_WritePin(GPIOB, GPIO_Pin_12, GPIO_PIN_RESET); \\
HAL_GPIO_WritePin(GPIOB, GPIO_Pin_13, GPIO_PIN_SET);}
#define MOTOR_STOP {HAL_GPIO_WritePin(GPIOB, GPIO_Pin_12, GPIO_PIN_RESET); \\
HAL_GPIO_WritePin(GPIOB, GPIO_Pin_13, GPIO_PIN_RESET);}
// 设置速度(0~999 对应 0%~100%)
void Motor_SetSpeed(uint16_t duty) {
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty);
}
2. 速度读取(10ms 周期调用)
#define PPR 510
#define CPR (4 * PPR) // 4× 倍频
int32_t g_ext_count = 0; // 32 位扩展计数
uint16_t g_last_cnt = 0;
float Motor_GetSpeed(void) {
uint16_t cnt = __HAL_TIM_GET_COUNTER(&htim4);
int32_t delta = (int32_t)cnt – (int32_t)g_last_cnt;
// 处理 16 位溢出
if (delta > 32768) delta -= 65536;
else if (delta < –32768) delta += 65536;
g_last_cnt = cnt;
// 10ms 周期 → 0.01s;RPM = Δcount / CPR / 0.01 * 60
return (float)delta / CPR / 0.01f * 60.0f;
}
3. 溢出中断处理
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM4) {
if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim4))
g_ext_count -= 65536;
else
g_ext_count += 65536;
}
}
4. PID 速度环(10ms 定时中断中调用)
typedef struct {
float Kp, Ki, Kd;
float err, err_last, integral;
float target;
} PID_Handle;
float PID_Calc(PID_Handle *pid, float feedback) {
pid->err = pid->target – feedback;
pid->integral += pid->err * 0.01f;
float derivative = (pid->err – pid->err_last) / 0.01f;
pid->err_last = pid->err;
float out = pid->Kp * pid->err + pid->Ki * pid->integral + pid->Kd * derivative;
if (out > 999) out = 999;
if (out < 0) out = 0;
return out;
}
// 10ms 定时器回调中:
extern PID_Handle motor_pid;
float speed = Motor_GetSpeed();
uint16_t duty = (uint16_t)PID_Calc(&motor_pid, speed);
Motor_SetSpeed(duty);
八、标准库代码(STM32F103)
1. 时钟与 GPIO 初始化
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3 | RCC_APB1Periph_TIM4, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);
// PB12/PB13 方向控制推挽输出
GPIO_InitTypeDef gpio = {0};
gpio.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13;
gpio.GPIO_Mode = GPIO_Mode_Out_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &gpio);
// PB8/PB9 编码器输入(浮空/上拉,依编码器类型)
gpio.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 若为 OC 输出则改用 IPU
GPIO_Init(GPIOB, &gpio);
// PA6 (TIM3_CH1) PWM 复用推挽
gpio.GPIO_Pin = GPIO_Pin_6;
gpio.GPIO_Mode = GPIO_Mode_AF_PP;
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio);
2. TIM3 PWM 配置
TIM_TimeBaseInitTypeDef tim = {0};
tim.TIM_Prescaler = 71;
tim.TIM_CounterMode = TIM_CounterMode_Up;
tim.TIM_Period = 999;
tim.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3, &tim);
TIM_OCInitTypeDef oc = {0};
oc.TIM_OCMode = TIM_OCMode_PWM1;
oc.TIM_OutputState = TIM_OutputState_Enable;
oc.TIM_Pulse = 0;
oc.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &oc);
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_Cmd(TIM3, ENABLE);
3. TIM4 编码器模式
TIM_TimeBaseInitTypeDef tim4 = {0};
tim4.TIM_Prescaler = 0;
tim4.TIM_CounterMode = TIM_CounterMode_Up;
tim4.TIM_Period = 65535;
tim4.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM4, &tim4);
// 4× 倍频:TI1 和 TI2 双边沿计数
TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
// 输入滤波
TIM_ICInitTypeDef ic = {0};
ic.TIM_Channel = TIM_Channel_1;
ic.TIM_ICFilter = 6;
TIM_ICInit(TIM4, &ic);
ic.TIM_Channel = TIM_Channel_2;
TIM_ICInit(TIM4, &ic);
TIM_Cmd(TIM4, ENABLE);
4. 使用
// 设置占空比
TIM_SetCompare1(TIM3, duty); // duty: 0~999
// 读取编码器
int16_t cnt = TIM_GetCounter(TIM4);
uint8_t dir = (TIM4->CR1 & TIM_CR1_DIR) ? 1 : 0; // 0=正转, 1=反转
// 方向控制
GPIO_SetBits(GPIOB, GPIO_Pin_12);
GPIO_ResetBits(GPIOB, GPIO_Pin_13); // 正转
九、开环 → 闭环:开发流程的四个阶段
- 阶段一:PWM 开环——固定占空比,验证电机能转、方向正确、L298N 不发热。占空比从 30% 起步,避免启动电流冲击。
- 阶段二:编码器计数——读取 TIM4->CNT,手动转电机轴,确认计数递增/递减与转向一致。若反向,交换 A/B 接线或翻转极性。
- 阶段三:速度计算——10ms 周期读取 Δcount,换算 RPM。用串口打印,观察速度是否稳定。
- 阶段四:PID 速度闭环——设定目标 RPM,PID 输出占空比。先调 Kp(从小到大),再调 Ki 消除稳态误差,Kd 一般可选。
十、避坑清单(实战干货)
- 电机啸叫:PWM 频率太低(< 5kHz),提高到 10kHz~20kHz
- 编码器计数乱跳:A/B 线未加滤波,提高 IC1F/IC2F 到 6~10;或 A/B 线太长引入干扰,缩短走线或改用差分信号
- STM32 反复复位:电机电源与逻辑电源未分开,或共地不良
- 方向反了:交换 A/B 接线,或翻转 IC1P/IC2P 极性(改代码比改线快)
- 16 位溢出:高速旋转时 CNT 会溢出,必须在更新中断里做 32 位软件扩展
- L298N 发热严重:导通压降 1.2~2V,效率较低,大功率场景建议换 TB6612 或 MOSFET 驱动
- PID 震荡:Kp 过大,逐步减小;或 10ms 控制周期太短,改为 20~50ms 试试
十一、文章收尾:三条可执行结论
感谢您的阅读!本号长期深耕电子信息、通信与嵌入式领域的技术分享,如果这篇文章对您有所启发,欢迎点赞、收藏并关注,后续将持续输出更多硬核干货,与您一同成长!






