☰
STM32 L298N 电机驱动代码落地:PWM配置、接线与排错
2026/10/21 21:20:57 网站建设 项目流程

简介:STM32搭配L298N驱动直流电机是嵌入式控制中非常典型的入门项目,常用于智能车、小型机器人等场景。这份代码包以按键控制电机转速与方向为任务,提供了一套从GPIO初始化、外部中断响应到定时器PWM调速的完整示例工程,适合初学STM32或需要快速搭建电机控制原型的开发者参考。压缩包共143个文件,以C源码、H头文件为主,辅以Keil工程配置文件、可烧录的HEX文件以及编译中间文件,整体约2.62MB,MDK环境下打开即可编译运行。示例代码演示了L298N使能与输入引脚的不同组合如何决定电机正反转,同时通过修改定时器比较值来改变PWM占空比,从而实现无级调速;相关驱动模块拆分清晰,便于对照原理图理解每个寄存器配置和中断处理逻辑。目前已有5739人学习下载,对于想梳理外设调用流程、掌握中断与PWM协作机制,或者直接复用这套代码的读者来说,都是很有价值的参考资料。

1. STM32 L298N 代码包怎么落地:先看接线、再谈调试

搜 STM32 L298N 代码的人,多半不是缺原理,是缺一份能直接烧进去跑的工程。这套资源就是一个完整的 STM32 驱动 L298N 双 H 桥的代码包,里面包含定时器 PWM 初始化、方向控制函数、加减速接口和接线说明。适合三种人:刚做完 CubeMX 点灯、准备上小车底盘的初学者;手里有 L298N 板子但想快速验证电机的人;以及被 PWM 频率、共地、跳线帽这些细节卡住的老手。我按这个工程包实际用到的思路,把硬件接线、PSC/ARR 计算、控制接口和排错过程拆成下面六章,你可以顺着章节搭线,也可以直接跳到第 5 章对着问题查。先说一句结论:L298N 本身没有协议,你给它的只是三样东西——两个方向电平、一个 PWM 占空比、一个共地参考,这三样少一样,电机都转不对。

2. 接线与引脚规划:一路电机占用哪三个 STM32 引脚

L298N 模块是双 H 桥,内部是两个独立的 H 桥电路,可以同时驱动两台直流电机。每一路电机需要三个控制信号:两个方向引脚 IN1/IN2,一个使能引脚 ENA 或 ENB。对 STM32 来说,方向引脚是普通 GPIO 推挽输出,使能引脚必须接到带定时器复用功能的引脚上,才能输出 PWM。注意一个容易被忽略的事实:L298N 的输入高电平门槛大约在 2.3V,STM32 的 3.3V GPIO 可以直接驱动,不用额外加电平转换芯片。这个结论让接线表变得很简单。

2.1 引脚分配表:PWM 引脚、方向引脚与地

我一般把 PA6/PA7 留给两路电机的 PWM,因为这两个引脚是 TIM3 的通道 1 和通道 2,在 F103 全系列上都有,重映射没什么争议。方向引脚随便挑普通 GPIO,PB8/PB9 和 PB10/PB11 不与常用调试口冲突。下面是推荐接线表:

功能STM32 引脚L298N 端子说明
电机 A 使能PA6ENATIM3_CH1,PWM 占空比控制速度
电机 A 方向PB8IN1GPIO 推挽输出,与 IN2 组合定方向
电机 A 方向PB9IN2GPIO 推挽输出
电机 B 使能PA7ENBTIM3_CH2
电机 B 方向PB10IN3GPIO 推挽输出
电机 B 方向PB11IN4GPIO 推挽输出
公共地GNDGNDSTM32 与 L298N 必须共地

这张表的核心是把 PWM 引脚放在定时器通道上,而不是随便找 GPIO。如果你在 CubeMX 里把 PA6 选成 TIM3_CH1,生成代码后定时器的复用功能会自动配好;如果手动建工程,就需要在 GPIO 初始化里手动把 PA6/PA7 设为复用推挽模式。方向引脚则完全不需要定时器资源,普通推挽输出就够,因为方向切换对时间精度没有要求。

为什么默认把 TIM4 留出来,而不是把 PWM 放在 TIM4 的 PB6/PB7 上?因为后面做闭环调速时,编码器最好挂在 TIM4 的编码器模式上,PWM 和编码器抢占同一个定时器会让引脚冲突,代码包里的注释也明确提醒了这一点。如果你只有一路电机且确定不做闭环,PWM 放 TIM4 也能跑,但最好一开始就按上面的表规划。

2.2 跳线帽和电源:先说清 7805 的使能条件

模块上的 ENA/ENB 跳线帽是新手第一个坑。出厂时跳线帽默认套在 5V 和使能引脚之间,等于把 enable 焊死在 5V,这时 PWM 信号进来会被忽略,电机永远是全速转。很多人改代码改了一个下午,最后发现是跳线帽没拔。拿到板子的第一步,把 ENA、ENB 两个跳线帽拔掉,把 STM32 的 PWM 输出线接到拔掉跳线帽后露出的使能引脚上。

电源方面,L298N 模块的 12V 端子就是电机主电源输入,建议和 STM32 的供电完全分开:电机用电池或独立适配器,STM32 用 USB 或稳压模块供电,两者只连一根 GND。模块板载的 7805 稳压器可以从电机电源取电生成 5V 逻辑电,但它的输入电压范围有限,一般建议电机电压在 7V 到 12V 之间时才允许使用板载稳压输出。如果电机电压超过 12V,必须把 5V 输出使能跳线帽拔掉,否则 7805 会严重发热甚至烧毁,此时逻辑电要从外部单独接到 L298N 的 5V 输入端子。

提示:L298N 板上的 ENA/ENB 跳线帽,拿到板子第一步就拔掉,除非你确认要全速直驱。

地线连接常常被当成玄学,但本质是电流回路。PWM 和方向电平都是 STM32 以自己的 GND 为参考输出的,L298N 内部逻辑也必须以同一个 GND 为参考才能正确解读这些电平。如果两边的地不连,信号电压会漂移,轻则电机抖动,重则烧毁 IO。连接时用粗一点的杜邦线或直接焊线,电机大电流造成的压降会在地线上产生噪声,线越细越容易出问题。

还需要提一下电流能力。L298N 每桥允许 2A 峰值电流,但连续电流超过 1.5A 后芯片温度会明显上升,必须加散热片。如果你带的电机额定电流超过这个范围,建议换大功率驱动板,代码包的接口设计对这一点留了替换空间。

3. 定时器 PWM 配置:PSC 和 ARR 的参数推导与典型值

定时器配置对新手是个黑匣子:同样的代码,改频率只要动两个数,但不知道这两个数怎么来的,就有种全凭玄学的感觉。其实只有一条公式:PWM 频率 = 定时器时钟 / ((PSC+1) x (ARR+1))。以 STM32F103 为例,APB1 上的定时器时钟是 72MHz,CubeMX 里把 PSC 设 7、ARR 设 899,算出来就是 10kHz,误差为 0。这个公式你一定要自己推一遍,因为你换一个主频不同的芯片时,别人给的配置就失效了。

3.1 PWM 频率与占空比分辨率:为什么不要用 1kHz

网上大量教程用 1kHz PWM,PSC=71、ARR=999,好算但不好用。直流电机用 1kHz 有两个毛病:低速时电流断续得厉害,电机会发出一顿一顿的顿挫感;PWM 的开关噪声落在人耳最敏感的频段,听起来像蚊子在耳边飞。10kHz 到 20kHz 是个平衡点。L298N 内部是 BJT 达林顿结构,不是 MOSFET,开关损耗随频率上升,但 20kHz 以下这个损耗完全可接受,所以把频率提到 20kHz 并不会把驱动芯片烧掉。

占空比分辨率由 ARR 决定:占空比最小步进是 1/(ARR+1)。ARR=899 时分辨率约 0.11%,对普通直流电机已经足够。有人喜欢把 ARR 拉到 9999 追求极致平滑,但这样 PWM 周期变成 10ms,调速响应变慢,做闭环时积分调节的反应也会拖后腿。常见的组合见下表,都基于 72MHz 定时器时钟:

目标频率PSCARR分辨率
1kHz719990.1%
10kHz78990.11%
20kHz38990.11%

我一般默认用 10kHz,兼顾噪声和开关损耗;如果项目对声音敏感,直接抄 20kHz 那一行,只改两个寄存器值,代码逻辑不用动。

3.2 CubeMX 初始化代码:TIM3 双通道 PWM 生成

CubeMX 里把 PA6 设为 TIM3_CH1、PA7 设为 TIM3_CH2,生成代码后主要逻辑固化在 MX_TIM3_Init 函数里。手动建工程的同学可以对照下面这段:

void MX_TIM3_Init(void) { TIM_OC_InitTypeDef sConfigOC = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 7; // 72MHz / (7+1) = 9MHz 计数器时钟 htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 899; // 9MHz / 900 = 10kHz htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(&htim3); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; // 初始比较值,占空比 0% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2); }

这个函数里几个参数要理解清楚:Prescaler 决定计数器从 72MHz 降到多快,Period 决定数多少个数溢出一次,两者相乘的倒数就是 PWM 频率。Pulse 是输出比较值,PWM1 模式下计数器值小于 Pulse 时输出有效电平,所以 Pulse=0 就是恒低电平,占空比为 0。AutoReloadPreload 开启后,运行时修改 Pulse 不会立刻生效,而是等计数器溢出进入下一次更新事件才统一加载,这个特性保证调速过程不会出现一个周期宽一个周期窄的撕裂脉宽。

代码生成后,别忘了在 main 函数里对两个通道调用 HAL_TIM_PWM_Start。这是新手漏得最多的一个函数:定时器初始化了,但输出通道没有使能,引脚上永远测不到波形。调用方式如下:

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);

之后调速只需要写比较值寄存器:__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty),duty 取值范围 0 到 899。这个宏是这套代码里被调用频率最高的底层操作,方向引脚负责向左还是向右,Pulse 负责转多快,两者互不干扰。

3.3 初始化顺序:GPIO 复用、速度等级和上电默认电平

定时器配置之前,引脚必须在 GPIO 层面设置成正确的模式。方向引脚和 PWM 引脚是两类完全不同的初始化:

GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_TIM3_CLK_ENABLE(); // 方向引脚:PB8/PB9/PB10/PB11,上电默认拉低,防止电机乱转 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11, GPIO_PIN_RESET); GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // PWM 引脚:PA6/PA7,必须复用推挽 GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

PWM 引脚设为复用推挽后,控制权交给定时器;方向引脚保持普通推挽输出。GPIO Speed 等级要注意:PWM 引脚选 HIGH 是为了让脉冲上升沿足够陡,避免边缘拖沓导致驱动芯片误判;方向引脚就选 LOW,减少电平切换时的电磁干扰。初始化顺序上,我习惯先配 GPIO 再初始化定时器,原因是如果定时器先把波形输出了,而 GPIO 复用还没设好,引脚电平处于不确定状态,电机可能在系统刚上电时抽动一下。方向引脚初始拉低也很关键,它保证上电瞬间 IN1/IN2 为 00,电机处于自由滑行状态而不是突然刹车抱死。

4. 控制代码落地:方向枚举、占空比接口、刹车与死区补偿

定时器配置好之后,应用层不应该直接操作寄存器。代码包里的电机控制模块把操作封装成两层:底层是 GPIO 电平和定时器比较值,上层是方向枚举和占空比数值。这样应用层只关心「向左转 30% 速度」,不用记住 PB8 是高还是低。

4.1 接口设计:MotorId、MotorDir 与 SetDuty 的分层

先定义两个枚举和一个初始化函数:

typedef enum { MOTOR_LEFT, MOTOR_RIGHT } MotorId_t; typedef enum { MOTOR_FORWARD, MOTOR_REVERSE, MOTOR_BRAKE, MOTOR_FREE } MotorDir_t; void Motor_Init(void); void Motor_SetDir(MotorId_t id, MotorDir_t dir); void Motor_SetDuty(MotorId_t id, uint16_t duty); // duty: 0 ~ ARR

Motor_SetDir 的实现核心是一个开关判断。以左电机为例,PB8 接 IN1,PB9 接 IN2:

void Motor_SetDir(MotorId_t id, MotorDir_t dir) { if (id == MOTOR_LEFT) { switch (dir) { case MOTOR_FORWARD: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET); break; case MOTOR_REVERSE: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET); break; case MOTOR_BRAKE: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8 | GPIO_PIN_9, GPIO_PIN_SET); break; case MOTOR_FREE: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8 | GPIO_PIN_9, GPIO_PIN_RESET); break; } } /* 右电机同理,换成 PB10/PB11 */ }

这里要理解四种方向的电学含义:IN1=1、IN2=0 时 H 桥正向导通,IN1=0、IN2=1 反向导通;IN1=IN2=1 时两个下桥臂关断、两个上桥臂导通,电机两端被短路,这就是刹车,也叫再生制动;IN1=IN2=0 时所有开关断开,电机自由滑行。自由滑行不是刹车,负载大的时候滑行会造成反向冲击,所以需要停车时应该用 BRAKE 而不是 FREE。有个常见误用是直接把 FREE 当停止来用,电机带着惯性转好几圈才停下来,这在底盘项目中会让定位精度直接报废。

速度写入用下面这个宏,它其实就是改定时器的输出比较寄存器:

#define MOTOR_SET_DUTY(id, duty) __HAL_TIM_SET_COMPARE(&htim3, \ ((id) == MOTOR_LEFT ? TIM_CHANNEL_1 : TIM_CHANNEL_2), (duty))

duty 范围必须限制在 0 到 ARR 之间,超过 ARR 的脉冲会让输出一直保持高电平,等效于全速,但这不是正常用法。为什么不在 SetDuty 里直接限制?因为宏定义在编译期替换,没有边界检查,所以实际工程里应用层会把速度先做一次 clamp,再调这个宏。代码包在 motor.h 里留了 SPEED_CLAMP 宏,就是为了强制做这层保护。

4.2 换向刹车与最小死区补偿:保护 L298N 也保护齿轮箱

换向是电机控制里最容易翻车的地方。从正转直接切反转,电机因惯性还在向前转,反电动势和母线电压叠加,会在 H 桥上产生很大的电流尖峰,轻则驱动板发出刺耳的咔声,重则把续流二极管打穿。正确的做法是状态机:方向不变时直接调占空比;方向改变时,先归零占空比,再刹车一个固定时长,最后切到新方向。代码包里的换向函数如下:

void Motor_ChangeDir(MotorId_t id, MotorDir_t newDir) { Motor_SetDuty(id, 0); // 占空比归零,断开动力 Motor_SetDir(id, MOTOR_BRAKE); // 刹车,让反电动势通过 H 桥短路泄放 HAL_Delay(30); // 重载时改到 80ms Motor_SetDir(id, newDir); // 再输出新方向 }

这个 30ms 延时是经验值:普通小车底盘齿轮间隙小,30ms 足够;带减速箱的重载结构,延时要加长到 80ms,否则切换瞬间齿轮箱还会被反扭力砸一下。你有示波器的话可以观察电流波形,换向时电流尖峰明显下降,说明刹车在起作用。

第二个问题是死区。直流电机在占空比很小的时候转不动,超过某个阈值后突然起步,表现在小车上是低速时一跳一跳。这不是 PWM 坏了,而是电机启动需要克服静摩擦和电枢电阻,存在一个最小启动占空比。实测下来 10kHz PWM 下,普通小型直流电机的最低启动点大约在 12% 到 15% 占空比。解决方式是给最小占空比做钳位:速度请求即使低于阈值,实际输出的占空比也保持在阈值附近,让电机保持转动。

#define MOTOR_MIN_DUTY 120 // 约 13%,ARR=899 时 void Motor_SetSpeed(MotorId_t id, int16_t speed) // speed: -899 ~ +899 { MotorDir_t dir = (speed > 0) ? MOTOR_FORWARD : MOTOR_REVERSE; uint16_t duty = (speed > 0) ? (uint16_t)speed : (uint16_t)(-speed); if (speed == 0) { Motor_SetDir(id, MOTOR_BRAKE); return; } if (duty < MOTOR_MIN_DUTY) duty = MOTOR_MIN_DUTY; if (duty > 899) duty = 899; Motor_SetDir(id, dir); Motor_SetDuty(id, duty); }

这段代码把方向和速度合并成一个带符号数接口,应用层不用分别调两个函数。注意 speed 为 0 时是刹车而不是自由滑行,这和大多数人的直觉相反,但更符合工程需求:请求停车时电机应该停住,而不是靠惯性继续溜。如果项目需要堵转保护,可以在上层加一个定时器,2 秒内速度没有到目标值就切断输出,这个钩子在代码包里已经留好位置。

5. 避坑排查:五个 L298N 驱动翻车现场的复现与修复

这一章是代码包排错文档里最常被翻的一页。我把常见问题合并成五条,每一条都是真实踩过的坑,按现象、原因、解决的顺序写,方便你对着查。

5.1 问题一:PWM 引脚测不到波形

现象:烧录后电机完全无反应,示波器探到 PA6,显示一直是低电平,占空比怎么改都没变化。

原因有两种,一是没调用 HAL_TIM_PWM_Start,定时器在跑但输出通道没使能;二是 CubeMX 里 PA6 被配成了普通 GPIO 输出,没有选 TIM3_CH1 的复用功能,引脚根本不归定时器管。

解决:确认 main 函数里有HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);检查 GPIO 初始化里 PA6 是不是 GPIO_MODE_AF_PP。如果两个都对但测不到波形,把初始化代码打断点看 htim3.State 是否为 HAL_TIM_STATE_BUSY,这个状态卡住多半是前面某处调过 HAL_TIM_PWM_Stop 没恢复。另外,示波器探头别夹到 GND 之前先确认线和模块共地了,没共地时波形是悬空噪声,不是 PWM。

5.2 问题二:占空比改了转速不变

现象:代码里 SetDuty(100) 和 SetDuty(800) 转速一样快,电机一直是全速,速度不受控。

原因:L298N 板上的 ENA 跳线帽没拔。跳线帽把使能引脚直接拉到 5V,等于告诉芯片「不用管 PWM,永远输出」,你给的占空比信号被完全忽略。这个问题我用血泪经验提醒所有读者:它不报错、不发热,表现为纯粹的不可调速。

解决:拔掉 ENA 和 ENB 的跳线帽,把 PWM 线接到拔掉跳线帽后的使能引脚上。判断是不是这个原因的方法很简单:用万用表量使能引脚对地电压,如果始终是 5V,就是跳线帽还在。

5.3 问题三:电机一转 MCU 复位

现象:跑车、带负载、或者踩一脚油门,STM32 突然重启,串口打印断掉,屏幕闪一下。

原因:电机电流大时,地线上的压降会把 MCU 的参考地拉偏。如果 STM32 和 L298N 之间只有一根又细又长的杜邦线作为 GND,电机启动瞬间几十安培的冲击电流会在这个地线上产生电位差,MCU 逻辑电平错乱,直接触发复位。电源共用也会放大这个问题,电机启动时把板级 5V 拉低,MCU 供电跌出工作范围。

解决:电机电源和逻辑电源彻底分开,电机用独立电池,STM32 用 USB 或稳压模块供电;STM32 与 L298N 之间用尽可能短的粗导线共地,最好直接在 L298N 端子旁边汇合。还有一个低成本措施:在电机正负两端并一个 104 陶瓷电容和一个 47uF 电解电容,吸收换向尖峰,这个措施能治不少「一启动就复位」的毛病。

5.4 问题四:电机电压升到 12V 以上板子反而罢工

现象:原来 9V 供电一切正常,换成 18V 电池组后模块完全没反应,电机一动不动。

原因:L298N 模块板载的 7805 稳压器输入电压超过安全范围。模块出厂时 5V 输出跳线帽默认套着,7805 从 12V 端子取电生成 5V 逻辑电;电压一路升到 18V,7805 严重过压,不是保护就是直接烧毁,逻辑电路断电导致 PWM 和方向信号都失效。

解决:电机电压高于 12V 时,必须先拔掉 5V 输出跳线帽,禁止板载稳压器从主电源取电,然后给逻辑供电端子单独接一个 5V 电压。如果发现 7805 周边电容鼓包或者芯片烫手,大概率已经损坏,需要换模块或飞线绕过稳压部分。这个坑很容易被误判成代码问题,因为逻辑电一没,所有控制信号都像石沉大海。

5.5 问题五:低速顿挫和换向火花

现象:低速占空比在 10% 左右时电机一顿一顿;倒车切前进时模块附近有放电声响,齿轮箱发出咔嚓声。

原因:低速顿挫有两个来源,一是 PWM 频率太低,电流断续严重,二是占空比落在启动死区内。换向火花和咔嚓声是因为没有刹车过渡,电机反电动势叠加在母线上,冲击 H 桥和齿轮间隙。

解决:把 PWM 频率提到 10kHz 以上,按第 3 章的公式重算 PSC 和 ARR;占空比用最小启动值钳位,不要低于 12%。换向必须走 Motor_ChangeDir 的刹车流程,先归零占空比、刹车 30ms、再切方向。重载机构把这个延时长到 80ms。如果换向火花还是明显,检查电机端子上的续流电容,别把 12V 输入端和电机端接反。

6. 进阶技巧:编码器闭环 PI 调速与整机验证

开环 PWM 控制能用,但遇到负载变化就露馅:小车爬坡掉速、履带过障碍卡顿,占空比没变,转速却掉了。这时要从开环升级成闭环,编码器反馈是关键。STM32F103 的 TIM4 支持编码器模式,硬件自动对 AB 相脉冲进行四倍频和判向,比在外部中断里数脉冲省太多 CPU。

6.1 编码器接线与 TI12 模式配置

编码器 A 相接 PB6,B 相接 PB7,这两个引脚恰好是 TIM4 的通道 1 和通道 2。把定时器配置成编码器模式后,CNT 寄存器会自动跟着电机转角增减:

TIM_Encoder_InitTypeDef sEncoderConfig = {0}; htim4.Instance = TIM4; htim4.Init.Period = 0xFFFF; htim4.Init.CounterMode = TIM_COUNTERMODE_UP; htim4.Init.Prescaler = 0; HAL_TIM_Encoder_Init(&htim4, &sEncoderConfig); sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_FALLING; sEncoderConfig.IC1Filter = 10; sEncoderConfig.IC2Filter = 10; HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);

IC 滤波器设为 10 可以滤掉换向时的毛刺,这是代码包在实测后补上的参数。CNT 是 16 位有符号叠加,读出来直接做差值就是方向带上符号的速度。因为这个功能占用了 TIM4,所以第 2 章的接线方案里 PWM 一定要放在 TIM3 上,两条线不能互相挤占,这也是我坚持那套引脚分配的原因。

6.2 简化 PI 参数与整机验证

速度环我用增量式 PI,10ms 周期读一次编码器差值,把目标速度和实际速度的误差送进 PI,输出直接作为 PWM 占空比:

int16_t Motor_SpeedPI(int16_t target, int16_t current) { static int16_t lastError = 0; static int32_t integral = 0; int16_t err = target - current; integral += err; integral = CLAMP(integral, -3000, 3000); int32_t out = 15 * err + integral / 2; // Kp=15, Ki=0.5 out = CLAMP(out, -899, 899); lastError = err; return (int16_t)out; }

Kp 取 15 是因为满量程 899,误差 50 时输出约 750,接近全占空比,响应够快又不至于一上来就饱和。Ki 用 integral/2 而不是常规的 Ki*integral,是为了避免浮点运算在 Cortex-M3 上拖慢中断。积分限幅必须做,否则遇到堵转,积分项会一路飙到几千,解除堵转后需要很长时间才能收敛,也就是所谓的积分饱和。

验证方法很简单:给定一个 500 的目标速度,用串口以 100Hz 打印实际速度,看曲线能否在 200ms 内进入 ±5% 误差带。如果超调太大,把 Kp 降到 8;如果稳态误差一直消不掉,把 Ki 从 0.5 提到 1.0。带负载后重新整定,不要把空载参数直接用在负载工况里。

从那以后我每次拿到 L298N 工程,都强制自己先走一遍检查清单:跳线帽拔了没、共地线够不够粗、HAL_TIM_PWM_Start 调了没、换向走没走刹车流程。这套 STM32 L298N 代码就是把这些检查固化成了代码层级,你现在照着这个流程搭一套,至少省掉一个通宵的调试时间。希望帮到你。

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

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

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

立即咨询