☰
STM32驱动L298N电机控制:PWM调速与正反转代码详解
2026/10/26 18:30:07 网站建设 项目流程

简介:这份资源面向嵌入式初学者与电机控制入门开发者,围绕STM32微控制器与L298N H桥驱动芯片的配合使用,解决按键控制直流电机转速与正反转的典型实践问题。压缩包共143个文件,约2.62MB,以35个h头文件与34个c源文件为核心,涵盖GPIO、定时器PWM、中断等外设驱动代码,另含Keil工程配置、编译中间文件与hex烧录文件,构成可直接编译运行的完整工程。已有5740人学习下载,说明其在入门实践中具有较高参考价值。资源中的示例工程完整呈现了按键中断响应、PWM占空比调节与L298N输入引脚状态切换的实现思路,读者可据此理解GPIO模式配置、定时器参数设定及电机正反转控制逻辑,并在此基础上调试优化,加深对软硬件协同工作的理解。

1. 拿到一份 STM32 L298N 代码包,先别急着烧录

上周有个做实训车的朋友把一份 STM32 L298N 代码包丢给我,说电机只会一个方向转,问我是不是代码写错了。我打开一看,PWM 初始化里定时器通道配反了,方向引脚又和 PWM 抢同一个端口,这种问题烧一百次也跑不对。这份资源就是围绕 STM32 驱动 L298N 双 H 桥的完整代码包,覆盖 GPIO 方向控制、定时器 PWM 调速、正反转与刹车逻辑,适合刚接触电机驱动的学生、做小车底盘的开发者,以及需要把现成驱动移植到自己板子上的从业者。它解决的不是“能不能转”,而是“转得稳、调得准、换板子还能用”。下面按我拆包复现的顺序讲,参数怎么设、坑在哪,都落到具体位置。

2. 拆开代码包看结构:L298N 到底怎么被 STM32 管住

2.1 先认清 L298N 的引脚语义,再谈代码

L298N 是双 H 桥,每个桥有 IN1/IN2 两个逻辑输入和 ENA 使能端。逻辑真值很直接:IN1 高、IN2 低正转,反过来反转,两个同高是刹车(快速停止),两个同低是惰行(自由滑行)。ENA 接 PWM 时,占空比决定输出平均电压,也就是转速。很多人翻车在把 ENA 直接拉高当使能,结果只能全速跑,调速功能形同虚设。

代码包里对每个电机抽象出三个引脚:方向 A、方向 B、PWM。这个抽象很关键,因为不同板子的引脚分配不一样,只要改宏定义就能换端口,不用动驱动逻辑。我一般会先画一张引脚映射表再动手,避免后面改一处漏一处。

信号L298N 侧STM32 侧常见接法作用
IN1电机 A 方向 1任意 GPIO 推挽输出正反转组合
IN2电机 A 方向 2任意 GPIO 推挽输出正反转组合
ENA电机 A 使能定时器 PWM 通道调速
IN3/IN4电机 B 方向任意 GPIO第二路电机
ENB电机 B 使能另一路 PWM第二路调速

注意 ENA/ENB 上的跳线帽,很多模块出厂默认短接到 5V,插着跳线帽接 PWM 是没反应的,得先拔掉。这个细节代码里不会写,但它是“代码没错电机不转”的头号原因。

2.2 定时器 PWM 的配置逻辑,别只抄 CubeMX 默认值

代码包里 PWM 走的是通用定时器,配置顺序是:开时钟、设预分频和自动重装、配通道为 PWM 模式、设比较值、使能输出、启动定时器。核心参数就两个:预分频 PSC 和自动重装 ARR,它们共同决定 PWM 频率:

f_pwm = f_clk / ((PSC + 1) * (ARR + 1))

L298N 的开关频率建议落在 1kHz 到 20kHz 之间。太低电机会有可闻啸叫和抖动,太高又受 H 桥开关损耗限制。我一般取 8kHz 到 10kHz,避开人耳敏感段又留足余量。假设主频 72MHz,取 PSC=71、ARR=99,得到 72M/(72*100)=10kHz,比较值范围 0 到 99,分辨率 1%,够用。

// 定时器 PWM 初始化(以 TIM3 通道1 为例,72MHz 主频) void MotorPWM_Init(void) { // 1. 开定时器和 GPIO 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 2. PWM 引脚复用推挽输出 GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_6; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); // 3. 时基:72M/(71+1)/(99+1) = 10kHz TIM_TimeBaseInitTypeDef tb; tb.TIM_Prescaler = 71; tb.TIM_Period = 99; tb.TIM_CounterMode = TIM_CounterMode_Up; tb.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, &tb); // 4. 通道1 设为 PWM1 模式,初始占空比 0 TIM_OCInitTypeDef oc; TIM_OCStructInit(&oc); 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_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }

这段代码里TIM_Pulse就是比较值 CCR,改它等于改占空比。TIM_OCPreload_Enable让新占空比在下一个周期生效,避免中途改值产生毛刺。TIM_OCPolarity_High表示比较值越大高电平越长,如果你的驱动电路是低有效,这里要改成 Low,否则调速方向是反的——这是第二个高频翻车点。

2.3 方向控制与调速的封装,把真值表写进函数

方向引脚用普通推挽输出即可,速度靠 PWM。代码包把两者封成一个Motor_Set(speed),speed 取 -100 到 100,正负表方向,绝对值表占空比。这样上层调用不用关心真值表,换电机、换接线只改底层。

// 方向引脚初始化(IN1=PA4, IN2=PA5) void MotorDir_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); GPIO_ResetBits(GPIOA, GPIO_Pin_4 | GPIO_Pin_5); // 默认惰行 } // speed: -100 ~ 100,正转正、反转负、0 惰行 void Motor_Set(int8_t speed) { uint16_t duty; if (speed > 100) speed = 100; if (speed < -100) speed = -100; if (speed > 0) { // 正转 GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_5); duty = speed; } else if (speed < 0) { // 反转 GPIO_ResetBits(GPIOA, GPIO_Pin_4); GPIO_SetBits(GPIOA, GPIO_Pin_5); duty = -speed; } else { // 惰行 GPIO_ResetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_5); duty = 0; } TIM_SetCompare1(TIM3, duty); // 0~100 映射到 CCR }

这里有个隐含假设:ARR 是 99,所以 duty 直接当 CCR 用。如果改了 ARR,映射要跟着改,写成duty * (ARR+1) / 100更稳妥。另外int8_t范围是 -128 到 127,限幅到 ±100 是安全的。刹车功能没放进这个函数,需要时单独调Motor_Brake()把两个方向脚都拉高,再配合 PWM 归零,急停场景才用,日常别乱刹车,H 桥电流冲击不小。

3. 把代码跑起来:从接线到闭环调速的完整步骤

3.1 供电与共地,先解决“上电就复位”

L298N 模块有 12V 电机供电和 5V 逻辑供电两路。常见做法是电机用独立电池经模块的 12V 端子,模块自带的 5V 输出给 STM32 供电——但要注意模块 5V 输出能力有限,带不动板子加外设时,STM32 会反复复位,现象是电机转一下停一下。血泪经验:逻辑供电和电机供电分开,至少保证共地,电机大电流回流不要走信号地。

接线顺序我固定为:先接共地,再接逻辑电源,最后接电机电源。上电前用万用表量一下 5V 和 GND 有没有短路,这一步省不得。电机线先不接,烧录后先用 LED 或示波器看 PWM 脚有没有波形,确认定时器跑起来了再接电机,能少烧几片驱动。

3.2 用串口指令验证正反转与占空比

代码包带一个简单的串口解析,收到字符+加速、-减速、0停、r换向。验证时不要一上来就接轮子,先空载看电机轴。步骤是:烧录、开串口、发+看转速是否阶梯上升、发r看是否反向、发0看是否惰行。如果方向对但速度不变,回去查 ENA 跳线帽和TIM_SetCompare1有没有被调用;如果速度变但方向不换,查方向脚是不是被别的外设复用了。

// 串口接收回调里的简易指令处理 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { char c = USART_ReceiveData(USART1); static int8_t spd = 0; if (c == '+') spd += 10; else if (c == '-') spd -= 10; else if (c == '0') spd = 0; else if (c == 'r') spd = -spd; Motor_Set(spd); USART_SendData(USART1, c); // 回显,确认收到 } }

回显这行别省,它是判断“指令到底进没进中断”的最快手段。串口没反应先查波特率和中断优先级,别急着怀疑电机代码。

3.3 加编码器做闭环,把“能转”变成“转得准”

开环调速在电池电压下降后转速会漂,做循迹或定距就得加编码器。代码包预留了编码器接口,用定时器的编码器模式读 AB 相。配置要点:把编码器对应的两个引脚设为浮空输入,定时器选编码器模式 TI1 和 TI2 都计数,ARR 设最大,定期读计数差算速度。常见做法是 10ms 读一次,差值除以线数得到转速,再进一个简单 PI 环修正 PWM。

// 定时器编码器模式初始化(TIM4 CH1/CH2 接编码器 A/B 相) void Encoder_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &gpio); TIM_TimeBaseInitTypeDef tb; tb.TIM_Prescaler = 0; tb.TIM_Period = 65535; tb.TIM_CounterMode = TIM_CounterMode_Up; tb.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM4, &tb); TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM4, 0); TIM_Cmd(TIM4, ENABLE); }

TIM_EncoderMode_TI12表示 AB 相都触发计数,四倍频,分辨率最高。读的时候用TIM_GetCounter取当前值,和上次值做差,注意计数器会溢出回绕,差值要用有符号处理。PI 参数先给 P=2、I=0.1 这种小值试,积分饱和会让电机在目标速度附近来回冲,必要时加积分限幅。

4. 避坑与排查:这几处翻车我见过太多次

4.1 电机不转,代码编译通过

现象:烧录成功,串口有回显,电机纹丝不动。原因多半是 ENA 跳线帽没拔,PWM 根本没进到使能端;或者方向脚和 PWM 脚在 CubeMX 里被分配到同一个引脚,后配置的覆盖了前者。解决:拔跳线帽,用万用表量 ENA 对地电压是否随占空比变化,再核对引脚分配表,一个引脚只归一个功能。

4.2 只有一个方向能转,反向没反应

现象:正转正常,发反向指令电机停住或抖动。原因通常是方向真值表写反,或者某个方向脚被配置成了复用功能而非普通输出。解决:单独把两个方向脚拉高拉低测电平,确认 GPIO 模式是Out_PP,再对照真值表逐行核对。别信“我肯定写对了”,用表量。

4.3 调速时电机啸叫、低速抖动

现象:占空比低时电机嗡嗡响甚至不转,占空比高才顺。原因是 PWM 频率落在可闻段,或者低速时平均电压不足以克服静摩擦。解决:把频率提到 8kHz 以上,低速段做死区补偿,占空比低于某个阈值(比如 15%)时直接给一个起步值,别让它卡在临界点。

4.4 一接电机 STM32 就复位

现象:空载正常,带电机就重启。原因是电机启动电流把逻辑电源拉垮,或者地线走法让回流串进信号地。解决:电机电源和逻辑电源分开供,共地单点连接,电机两端并一个续流二极管或 RC 吸收,电源入口加足够大的电解电容。这是硬件问题,改代码救不回来。

4.5 编码器读数只增不减或乱跳

现象:电机正反转编码器计数都往一个方向走,或者静止时数值乱跳。原因是 AB 相接线顺序反了,或者输入没上拉、受干扰。解决:对调 A/B 相两根线看方向是否恢复,输入改上拉或外部加 10k 上拉,信号线远离电机线走。计数乱跳还要查定时器是不是被别的功能复用了。

5. 进阶:把驱动层抽出来,换板子只改一个头文件

代码包最值钱的地方不是那几行 PWM,而是把“引脚映射”和“驱动逻辑”分开了。我后来把它重构成一个motor_port.h,里面只有宏定义和三个函数声明,换 STM32 型号或换引脚时只动这个文件,motor.c一行不改。这个习惯救过我很多次——同一套底盘代码在 F1 和 F4 上跑,只改时钟和引脚宏,半天就能移植完。

验证驱动是否真的解耦,我有个笨办法:故意把 PWM 引脚宏改到一个没接电机的空闲脚,编译烧录,如果除了电机不转之外没有任何编译错误和运行异常,说明抽象是干净的;如果一改就报一堆未定义,说明逻辑里还硬编码了端口。这个方法比读代码快。

重构前重构后收益
端口号散落在各函数集中在motor_port.h换板只改一处
占空比直接写 CCR统一 0~100 映射改 ARR 不影响上层
方向真值表内联封装成Motor_Set调用方不碰硬件

还有一个细节:PWM 启动前先把比较值清零再使能定时器,能避免上电瞬间电机猛冲一下。这个“上电冲击”在带轮子的车上表现为突然窜一下,很吓人,加一行TIM_SetCompare1(TIM3, 0)就压住了。从那以后我每次写电机初始化,都强制先归零再使能,这个顺序已经成了肌肉记忆。希望这份拆解帮到你,少走我当年那些弯路。

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

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

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

立即咨询