☰
STM32驱动L298N电机:PWM调速与差速控制实战
2026/10/26 10:35:53 网站建设 项目流程

简介:这份资源面向嵌入式初学者与电机控制方向的开发者,围绕STM32微控制器与L298N H桥驱动芯片的配合使用,解决按键控制直流电机转速与正反转的典型工程问题。压缩包共143个文件,约2.62MB,以C源码与头文件为主体,涵盖GPIO初始化、定时器PWM配置、按键中断处理及L298N控制逻辑,同时包含Keil工程文件、编译中间产物与烧录用的hex文件,便于直接打开工程复现实验。已有5740人学习下载,说明其在入门实践中具备一定参考价值。资源中的示例工程完整呈现了从引脚配置到占空比调节、方向切换的实现链路,读者可据此理解PWM调速原理、中断响应流程与驱动芯片的输入组合逻辑,并在此基础上调试优化,加深对软硬件协同工作的认识。

1. 从一块吃灰的 L298N 模块说起:这套 STM32 驱动代码到底能干什么

手头大概率都躺着一块红色或绿色的 L298N 双 H 桥模块,旁边还有两个减速电机和一块 STM32 最小系统板。真正卡住人的往往不是接线,而是代码:PWM 该挂在哪个定时器、正反转怎么组合、占空比给多少电机才不抖。这套 STM32 L298N 代码就是冲着这个场景来的,它把 GPIO 方向控制和 TIM 通道 PWM 调速封装成可直接调用的接口,覆盖单电机正反转、双电机差速、占空比线性调速这几件事。适合刚上手 STM32 外设、准备做第一台差速小车或履带底盘的开发者,也适合想把裸机驱动整理成可复用模块的老手。下面按“能干什么 → 怎么跑 → 怎么改”的顺序拆开讲。

2. 硬件连接与工程结构:先让电机安全转起来

2.1 L298N 与 STM32 的引脚对应关系

L298N 是双 H 桥,每个 H 桥有 IN1、IN2 两个方向输入和 ENA 使能端。方向端接普通 GPIO,使能端接定时器 PWM 输出,这是最省事也最稳的组合。常见做法是把 ENA、ENB 分别挂到 TIM 的两个通道上,方向端随便挑两个推挽输出脚。下面这张表是我一般会先画在纸上的对应关系,接线前对一遍能省掉一半返工。

L298N 引脚作用STM32 侧配置要点
IN1 / IN2电机 A 方向PA0 / PA1推挽输出,初始低电平
IN3 / IN4电机 B 方向PA2 / PA3推挽输出,初始低电平
ENA电机 A 调速TIM2_CH1 (PA5)复用推挽,PWM 模式 1
ENB电机 B 调速TIM2_CH2 (PA1 复用)复用推挽,PWM 模式 1
GND共地GND必须与 STM32 共地

注意 ENA/ENB 上的跳线帽。模块出厂时这两个位置通常插着跳线帽,意思是使能端被硬拉高、全速运行。要接 PWM 就必须把跳线帽拔掉,否则你代码里占空比怎么改电机都一个速度,这个坑我第一次调的时候卡了整整一个下午。

2.2 工程目录与分层

这套代码按裸机工程组织,不依赖 HAL 之外的第三方库,目录结构大致如下。分层的目的很直接:换芯片型号时只动底层,上层控制逻辑不用碰。

project/ ├── Core/ │ ├── Inc/ │ │ ├── motor.h # 电机控制对外接口 │ │ └── main.h │ └── Src/ │ ├── motor.c # 方向 + PWM 封装 │ ├── tim.c # 定时器 PWM 初始化 │ ├── gpio.c # 方向引脚初始化 │ └── main.c # 业务调用示例 ├── Drivers/ │ └── STM32F1xx_HAL_Driver/ └── MDK-ARM/ # 或 STM32CubeIDE 工程文件

motor.c只做两件事:根据传入的方向枚举设置 IN1/IN2 电平,根据速度值写比较寄存器。tim.c负责把定时器配成 1 kHz 左右的 PWM 频率,这个频率对 L298N 来说比较友好,太低会有可闻啸叫,太高开关损耗和发热都上来。我一般把预分频和自动重装值算好写死,并在注释里标清楚当前主频,换板子时改这两个数就行。

2.3 定时器 PWM 初始化代码

下面这段是 TIM2 双通道 PWM 的核心初始化,基于 HAL 库,F1 系列通用。逻辑是先开时钟、配时基、配通道输出模式,最后启动。参数含义我写在注释里,照着改主频和频率即可。

/* tim.c —— TIM2 双通道 PWM,目标频率 1kHz,主频 72MHz */ void MX_TIM2_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; TIM_OC_InitTypeDef sConfigOC = {0}; htim2.Instance = TIM2; /* 72MHz / (71+1) = 1MHz 计数频率 */ htim2.Init.Prescaler = 71; /* 1MHz / (999+1) = 1kHz PWM 频率 */ htim2.Init.Period = 999; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(&htim2); sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig); HAL_TIM_PWM_Init(&htim2); sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig); sConfigOC.OCMode = TIM_OCMODE_PWM1; /* 计数值小于 CCR 时输出有效电平 */ sConfigOC.Pulse = 0; /* 初始占空比 0,电机不转 */ sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); }

Prescaler和Period是调速精度的来源:Period 设成 999,占空比就有 0~1000 共 1001 档,够细了。Pulse就是比较值,后面调速本质上是改它。OCMode选 PWM1 意味着计数值小于 Pulse 时输出高电平,L298N 使能端高有效,逻辑正好对上。如果换到 F4 系列,主频变成 168MHz,预分频要重算,别直接抄。

2.4 电机控制接口封装

方向控制和调速封装成一个结构体加几个函数,调用方不用关心底层是哪个定时器。下面这段是motor.c的核心,正反转靠 IN1/IN2 电平组合,停止时两脚都拉低并清占空比。

/* motor.c —— 单电机抽象 */ typedef enum { MOTOR_STOP = 0, MOTOR_FORWARD, MOTOR_BACKWARD } MotorDir_t; typedef struct { GPIO_TypeDef *in1_port; uint16_t in1_pin; GPIO_TypeDef *in2_port; uint16_t in2_pin; TIM_HandleTypeDef *htim; uint32_t channel; } Motor_t; /* 设置方向与速度,speed 范围 0~1000 */ void Motor_Set(Motor_t *m, MotorDir_t dir, uint16_t speed) { if (speed > 1000) speed = 1000; /* 上限保护,防止越界写 CCR */ switch (dir) { case MOTOR_FORWARD: HAL_GPIO_WritePin(m->in1_port, m->in1_pin, GPIO_PIN_SET); HAL_GPIO_WritePin(m->in2_port, m->in2_pin, GPIO_PIN_RESET); break; case MOTOR_BACKWARD: HAL_GPIO_WritePin(m->in1_port, m->in1_pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(m->in2_port, m->in2_pin, GPIO_PIN_SET); break; default: /* MOTOR_STOP */ HAL_GPIO_WritePin(m->in1_port, m->in1_pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(m->in2_port, m->in2_pin, GPIO_PIN_RESET); speed = 0; break; } __HAL_TIM_SET_COMPARE(m->htim, m->channel, speed); }

Motor_t把方向脚和 PWM 通道绑在一起,两个电机就实例化两份,差速控制时分别调Motor_Set即可。speed上限保护是血泪经验:CCR 超过 Period 在某些系列上行为不确定,先夹住最稳。停止时把方向脚也拉低,是为了避免电机两端悬空导致的反电动势干扰,尤其是带减速箱的大扭矩电机。

3. 主循环与差速控制:把两个电机跑成一台车

3.1 初始化顺序与调用示例

上电顺序有讲究:先配 GPIO 和定时器,再实例化电机结构体,最后进主循环。顺序反了会出现上电瞬间电机抽一下的现象,因为方向脚还是浮空状态。下面这段main.c是能直接跑的最小示例,两个电机先正转再反转,中间留延时方便观察。

/* main.c —— 双电机差速示例 */ Motor_t motorL = { GPIOA, GPIO_PIN_0, GPIOA, GPIO_PIN_1, &htim2, TIM_CHANNEL_1 }; Motor_t motorR = { GPIOA, GPIO_PIN_2, GPIOA, GPIO_PIN_3, &htim2, TIM_CHANNEL_2 }; int main(void) { HAL_Init(); SystemClock_Config(); /* 72MHz */ MX_GPIO_Init(); MX_TIM2_Init(); while (1) { /* 直行:两侧同速 */ Motor_Set(&motorL, MOTOR_FORWARD, 600); Motor_Set(&motorR, MOTOR_FORWARD, 600); HAL_Delay(2000); /* 右转:左轮快、右轮慢,形成差速 */ Motor_Set(&motorL, MOTOR_FORWARD, 800); Motor_Set(&motorR, MOTOR_FORWARD, 300); HAL_Delay(1500); /* 原地停 */ Motor_Set(&motorL, MOTOR_STOP, 0); Motor_Set(&motorR, MOTOR_STOP, 0); HAL_Delay(1000); } }

差速转向的原理就是两侧轮速差,Motor_Set的 speed 参数直接对应占空比,600 和 300 的差值决定了转弯半径。实际调车时先给固定占空比测出死区——很多减速电机在 200 以下根本不转,低于这个值直接给 0,否则会出现“指令给了但车不动”的假故障。

3.2 速度斜坡与启停平滑

直接从一个占空比跳到另一个,电机和电源都会被冲击,电池供电时甚至会把 MCU 拉复位。常见做法是加一个软件斜坡,每 10ms 逼近目标值一小步。下面这个函数放在定时器中断或主循环里调用都行,step控制加速快慢。

/* 速度斜坡:current 向 target 逼近,每次最多变化 step */ uint16_t Speed_Ramp(uint16_t current, uint16_t target, uint16_t step) { if (current < target) { current += step; if (current > target) current = target; } else if (current > target) { current -= step; if (current < target) current = target; } return current; }

step取 20~50 比较合适,太小响应肉,太大又失去平滑意义。这个函数不碰硬件,纯数值处理,方便单测。我一般把它和Motor_Set配合,在 10ms 周期的任务里先算斜坡再下发,启停和换向都顺很多。

3.3 换向时的死区处理

正转直接切反转是另一个高频翻车点。H 桥上下管如果同时导通会有直通风险,虽然 L298N 内部有一定保护,但电流冲击依然存在。稳妥做法是换向前先给一段全停时间,让电机电流衰减,再切反向。下面这段是换向封装,dead_ms一般给 50~100ms。

/* 安全换向:先停再反向 */ void Motor_ReverseSafe(Motor_t *m, uint16_t speed, uint32_t dead_ms) { Motor_Set(m, MOTOR_STOP, 0); HAL_Delay(dead_ms); /* 等待电流衰减 */ Motor_Set(m, MOTOR_BACKWARD, speed); }

dead_ms不是越大越好,太大车会有明显顿挫。实测 50ms 对大多数小减速电机够用,大扭矩电机可以加到 100ms。这个延时是阻塞的,如果主循环里有其他实时任务,建议改成状态机加时间戳判断,别在主循环里硬HAL_Delay。

4. 避坑与排查:调电机时最容易翻车的几件事

4.1 电机不转但代码没报错

现象是下载程序后电机纹丝不动,示波器量 ENA 有波形。原因八成是 L298N 上的使能跳线帽没拔,使能端被硬拉高,PWM 根本没起作用;也可能是模块和 STM32 没共地,PWM 信号没有参考电平。解决方法是拔掉跳线帽、用万用表通断档确认 GND 连通,再量 ENA 对地电压是否随占空比变化。

4.2 电机转但方向反了

现象是给正转指令车却后退。原因通常是 IN1/IN2 接反,或者电机线红黑接反,两者叠加还会出现“代码里正转、实际也正转”的假象。解决办法是先在代码里只给方向不给速度,用手感受电机轴转向,确认方向逻辑后再加 PWM;接线错误就在硬件侧调换,别在代码里用取反硬凑,后面维护会乱。

4.3 一加速就复位或重启

现象是占空比一超过某个值,MCU 就重启,串口打印能看到复位标志。原因是电机启动电流把电源电压拉低,MCU 欠压复位。解决办法是电机电源和 MCU 电源分开供电、共地,电机侧并一个大电容(常见 470μF~1000μF),软件上加 3.2 节的斜坡,别让占空比一步到位。

4.4 PWM 频率选错导致啸叫或发热

现象是电机发出刺耳啸叫,或者模块发热明显。原因是 PWM 频率落在人耳敏感区间(几百 Hz 到几 kHz),或者频率太高导致 L298N 开关损耗大。解决办法是把频率定在 1kHz 附近,避开啸叫又不至于太热;如果对噪声敏感,可以试 8kHz 以上,但要同步检查模块温度。

4.5 占空比给满电机反而没劲

现象是占空比 1000 时电机转速不如 800 时。原因可能是电源功率不足,满占空比时电流需求最大、电压跌落最狠;也可能是定时器配置里 Period 和实际比较值不匹配。解决办法是先量满占空比时电机两端电压,明显低于电源电压就是供电问题,换更大电流的电源或加电容。

5. 二次开发:把这套驱动改成能复用的底盘控制层

这套代码跑通之后,真正有价值的是往上叠一层。我一般会把Motor_t再包一层Chassis_t,把“前进、后退、左转、右转、原地旋转”这些动作变成函数,底层还是调Motor_Set。这样换底盘(两轮差速、四轮麦轮)时只改Chassis_t的实现,上层遥控或循迹逻辑不动。下面这个结构体是我常用的形态,trim用来补偿左右电机个体差异,实测很有用。

typedef struct { Motor_t left; Motor_t right; int16_t trim; /* 左右补偿,范围 -100~100 */ } Chassis_t; void Chassis_Forward(Chassis_t *c, uint16_t speed) { int16_t l = (int16_t)speed + c->trim; int16_t r = (int16_t)speed - c->trim; if (l < 0) l = 0; if (l > 1000) l = 1000; if (r < 0) r = 0; if (r > 1000) r = 1000; Motor_Set(&c->left, MOTOR_FORWARD, (uint16_t)l); Motor_Set(&c->right, MOTOR_FORWARD, (uint16_t)r); }

trim的标定方法很土但有效:让车直行两米,看它往哪偏,往左偏就把 trim 调正让右轮快一点,反复几次直到偏差可接受。验证改造是否成功,我一般看三个指标:直行两米偏移小于 5cm、原地旋转时两轮方向相反且速度对称、急停后电机无续转。改完别急着上车,先把车架空跑一遍所有动作,听声音、看转向,确认无误再落地。

从那以后我每次改电机驱动,都强制先架空、只给方向不给速度、再逐步加占空比走一遍,这套流程帮我省掉了不少烧模块的后悔药。希望帮到你。

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

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

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

立即咨询