STM32小车电机驱动实战:L298N接线与PWM调速全解析

发布时间:2026/8/31 4:05:03
STM32小车电机驱动实战:L298N接线与PWM调速全解析 简介本资源是一套面向嵌入式初学者与STM32实践者的C语言电机驱动工程专为正点原子STM32F103核心板适配L298N双H桥驱动模块设计解决直流电机正反转、差速拐弯及PWM无级调速等典型控制需求适用于智能小车、机器人底盘等硬件项目开发。压缩包共76个文件含33个头文件.h定义外设配置与函数接口、32个源文件.c实现GPIO初始化、PWM输出、电机状态切换等核心逻辑另有Keil工程配置.uvprojx/.uvoptx、启动文件、系统库及LICENSE等完整开发支撑文件总大小312KB。已有125人下载学习。读者可直接导入Keil MDK编译运行获得可验证的电机控制功能代码框架涵盖GPIO推挽输出配置、TIM定时器PWM生成、IN1–IN4电平组合逻辑封装及左右轮独立调速函数目录结构遵循正点原子标准模板CORE/SYSTEM/HARDWARE/USER便于理解STM32固件库工程组织方式与电机驱动分层设计思想。 给单片机小车写驱动绕不开的坎就是电机控制。我从一块正点原子F103核心板和一块L298N驱动模块开始把正反转、差速拐弯、PWM调速这几个功能全部跑通前前后后踩了不少坑今天把这套驱动程序的完整设计思路、接线方案、代码实现和排查经验一次性整理出来。这篇东西写给刚入门STM32、想玩小车但卡在电机驱动这一层的朋友也写给那些已经能点灯但一接电机就“冒烟”的同学。先说结论L298N驱动直流电机这件事本身不难难的是把GPIO方向控制、定时器PWM调速、按键交互逻辑这三块整合在一起还要保证小车跑起来不抖、不偏、不烧板子。我的这套驱动用标准库风格实现代码量不大但每一行后面都有原理支撑照着抄能跑理解了能改。1. 项目背景与整体设计思路1.1 做这个小车驱动要解决什么问题很多人第一次接触STM32都是从点灯开始的可点完灯之后想做个能跑的小车突然发现电机驱动这块的资料特别碎有的教程只讲L298N怎么接不讲代码怎么写有的代码写出来了但一上电电机嗡嗡叫就是不转还有的转向逻辑搞得特别复杂根本看不懂。我的核心诉求很简单用正点原子F103核心板作为主控通过L298N驱动两个直流减速电机实现三个最基本的功能——电机正反转、小车拐弯、速度连续可调。这三个功能是任何轮式机器人的地基搞懂了之后后面做循迹、避障、遥控小车都是在这个基础上加传感器和逻辑而已。这个项目中真正值得研究的是两个层面的问题硬件层面是STM32和L298N怎么正确匹配包括电平、电源、共地这些容易踩坑的地方软件层面是如何把“方向”和“速度”这两个控制维度分离——方向控制走GPIO电平速度控制走定时器PWM两者互不干扰这样的架构写出来的代码才清晰后面加功能也容易。1.2 硬件选型为什么是F103加L298N的组合正点原子F103核心板用的是STM32F103系列芯片72MHz主频内置多个定时器每个定时器又能输出多路PWM对于控制两路直流电机这种场景来说性能和资源都绰绰有余。市面上很多小车套件用的就是这款芯片资料多、例程全、出了问题也好查。L298N则是经典的电机驱动芯片内部集成双H桥一片就能独立驱动两个直流电机。它和单片机之间只需要四根方向控制引脚加两根PWM使能引脚逻辑非常简单。很多新手会纠结要不要换成TB6612或者DRV8833这类更“现代”的驱动芯片——确实它们体积更小、压降更低但L298N胜在皮实耐操、引脚排列清晰、出错容错率高作为理解电机驱动原理的第一个芯片再合适不过了。选型还有一层考虑正点原子核心板已经把最小系统、调试接口、按键和LED都做上去了省去了自己焊最小系统的麻烦调试时直接用板载按键模拟控制信号连JLINK或STLINK就能下载程序可以让我把精力全部集中在电机驱动程序本身的逻辑上而不是浪费在外围电路排查上。1.3 软件架构把“方向控制”和“速度控制”拆开写驱动最忌讳的就是把所有功能堆在一起。我一开始也犯过这个毛病在一个函数里既翻转GPIO又修改PWM占空比结果想改个转速还得在控制逻辑里翻来翻去。后来重构了一把把软件拆成三个层次。底层是硬件抽象层负责初始化GPIO和定时器提供“设置某个引脚高/低电平”“设置某路PWM占空比”这样的原子操作。中间是电机控制层把底层操作组合成有意义的电机动作比如电机正转、反转、停止、左侧减速、右侧减速。最上层是业务逻辑层处理按键输入、模式切换这类交互逻辑它只需要调用中间层提供的函数接口完全不用关心硬件细节。这样分层的直接好处是调试方便小车不动了先查最底层GPIO和PWM有没有波形输出再查中间层控制函数有没有被正确调用最后才查业务逻辑。每一层都可以独立测试问题定位速度快很多。后面想加超声波避障或者红外循迹也只需要在业务逻辑层加判断完全不碰底层驱动代码。这套思路其实就是嵌入式开发里经典的分层设计花小车的钱提前养成好习惯值。2. 硬件接线与模块原理2.1 L298N内部H桥到底是怎么转的先不急着接线把L298N内部的东西搞清楚。L298N内部是两个完整的H桥电路每个H桥由四个开关管组成电机接在H桥的中间横臂上。所谓“正转”就是让对角线上的两个开关管导通电流从电机一端流向另一端所谓“反转”就是换另一条对角线导通电流方向反过来。对应到L298N模块的引脚上每个电机有三个控制输入端IN1、IN2和ENA。其中IN1和IN2负责控制H桥四个开关管的组合状态ENA是使能端。当ENA为高电平时H桥电路才真正接通IN1和IN2的电平组合才有意义当ENA为低电平时不管IN1和IN2是什么状态电机都不会转相当于被“关断”了。所以就有了最关键的设计思路用两个GPIO引脚给IN1、IN2输出高低电平决定电机转动的方向用一路PWM信号接到ENA通过改变PWM的占空比来控制电机的平均电压从而实现调速。方向控制是开关量速度控制是模拟量两种不同性质的信号各走各的通道互不干扰这就是整个驱动程序的底层逻辑。2.2 F103核心板引脚规划与接线表引脚规划上我遵循一个原则方向控制引脚随便选普通GPIO就行但PWM引脚必须挑定时器的通道引脚。STM32F103的每个定时器输出通道都有对应的引脚映射比如TIM3的CH3在PB0上CH4在PB1上。我选PB0和PB1作为两路PWM输出用来接L298N的ENA和ENB。方向和使能引脚的分配是这样的功能L298N引脚STM32引脚左电机方向1IN1PA4左电机方向2IN2PA5左电机使能/调速ENAPB0 (TIM3_CH3)右电机方向1IN3PA6右电机方向2IN4PA7右电机使能/调速ENBPB1 (TIM3_CH4)电源地GNDGND电机供电我用的是一块12V锂电池组直接接L298N的12V输入端子。L298N模块上的5V输出引脚给F103核心板供电但前提是L298N的12V输入足够稳定。这个方案接线少、使用简单但后面我会专门讲为什么实际跑车时我建议给单片机单独供电别长期依赖L298N的5V输出。2.3 电源与共地问题最容易烧板子的一步接线接错最容易出的问题就是电源。L298N模块上通常有VCC、GND和5V三个电源相关的端子。VCC接电机电源范围通常是7V到12V我实测12V没问题GND是电源地5V是模块自带的稳压输出可以给外部逻辑电路供电。这里有个至关重要的细节单片机的GND必须和L298N的GND连接在一起。只有两边共地了单片机输出的高电平对L298N来说才是真正的高电平。如果地不共单片机认为的3.3V高电平在L298N参考地眼里可能只有2V甚至更低导致H桥逻辑判断混乱电机要么不动要么乱抖。我见过很多新手接完线电机死活不转最后发现就是一根GND跳线没插。还要注意不要把L298N的5V输出当成核心板的唯一供电来源。如果你用的电机是12V大功率减速电机启动瞬间电流可能到1A以上L298N的稳压芯片压力很大输出的5V可能被拉垮连带单片机复位重启。所以我实际跑车的时候给F103核心板单独接了一路5V电源L298N的5V输出只作为一个参考电源或干脆不接。3. 驱动核心实现正反转与PWM调速3.1 GPIO初始化方向控制的基础方向控制的本质就是让GPIO引脚输出高电平或者低电平。STM32的GPIO在使用前必须初始化告诉芯片这个引脚是输入还是输出、输出模式是什么、速度多快。对于L298N这种逻辑输入引脚我全部配置成推挽输出模式频率选50MHz就够用了。GPIO初始化代码中有一个关键点是看引脚所在的端口总线。STM32的GPIO挂在APB2总线上所以初始化前必须开启对应端口的时钟。我以前刚学的时候经常忘了开时钟结果引脚电平怎么都拉不上去后来才明白是时钟没打通寄存器操作根本没生效。初始化完成后可以先用万用表量一下引脚电压来验证配置是否正确。程序里把IN1引脚拉高、IN2拉低用万用表测PA4应该能读到大约3.3V电压PA5应该接近0V。这一步验证做完才说明GPIO层面的驱动是通的再去接L298N就不会一头雾水了。3.2 定时器PWM配置转速怎么调PWM调速的原理是改变一个周期内高电平所占的比例这个比例就叫占空比。电机两端看到的平均电压 电源电压 × 占空比占空比越高电机转速越快。STM32的定时器产生PWM不需要CPU干预只要配置好定时器的工作模式和比较值硬件会自动翻转引脚电平这大大减轻了CPU负担。我使用TIM3的CH3和CH4输出两路PWM。定时器配置时有三个关键参数预分频系数PSC、自动重装载值ARR、比较值CCR。这三个值决定了PWM的周期和占空比。PSC用来把72MHz的系统时钟分频到一个合理的计数频率ARR决定计数周期CCR则是比较值当计数器计数到CCR时输出电平翻转。具体数值这样算我想让PWM频率在1kHz左右。定时器输入时钟是72MHz先经过预分频PSC设为71实际上是72分频计数频率变成72MHz ÷ 72 1MHz也就是计数器每1微秒加1。然后ARR设为999计数器从0数到999刚好需要1000微秒所以PWM频率就是1kHz。此时把CCR设为500占空比就是500/1000 50%电机得到一半的平均电压。那为什么选1kHz而不是更高频率L298N使用的续流二极管和开关管的开关速度有限频率太高会导致开关损耗增大、管子发热频率太低又会让电机产生“咔咔”的抖动声。实测下来1kHz到10kHz之间都是可以接受的我选1kHz只是为了方便示波器观察和计算你可以根据自己的电机特性调整。3.3 电机控制函数与拐弯逻辑方向控制函数本质上是IN1、IN2引脚的逻辑组合。同一个电机IN1高、IN2低为正转IN1低、IN2高为反转两个引脚电平相同则电机制动停止。我封装了三个基础函数分别对应正转、反转和停止这样业务逻辑里就不需要关心具体引脚了。对于两个电机的小车底盘正转前进和反转后退直接调用基础函数就行。但拐弯就稍微复杂一点我用了差速转向的方案左转的时候左侧电机速度降低右侧电机速度保持不变这样两边轮子转速不同小车就向左拐弯右转同理反过来。速度差越大转弯半径就越小。这种拐弯方式的优点是不需要额外硬件完全靠软件调节PWM占空比实现。我把差速比例抽成一个参数比如左转时左侧PWM占空比设为目标速度的40%右侧保持100%实测下来转弯半径大约在30厘米左右比较适合需要机器人原地转弯的场景。如果想要更急的转弯可以让一侧电机反转两个轮子朝相反方向转动小车就能原地转圈这个后续按需扩展就行。4. 完整代码与关键实现4.1 代码结构说明整个工程分成三个模块main.c负责主循环和按键逻辑motor.c负责电机相关初始化和控制函数tim_pwm.c负责定时器PWM的初始化。这样的分离让每个模块的职责非常单一代码可读性高也方便后面移植。我在正点原子F103核心板例程的工程模板基础上修改复用了它的按键初始化和延时函数。时钟频率方面系统默认是外部晶振倍频到72MHz这也是正常开发时的标准配置。核心的电机和PWM代码都是自己写的不依赖例程方便你看明白每一步在干什么。4.2 关键代码逐段解读先看定时器PWM初始化函数这个函数的作用是把PB0和PB1配置成TIM3的CH3和CH4输出工作模式是PWM1模式。PWM1模式指的是计数器计数到CCR之前输出高电平之后变低电平占空比越大平均电压越高。void TIM3_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // PB0和PB1复用为定时器输出功能 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 定时器基础配置计数器向上计数到arr后自动清零 TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // CH3和CH4都配置为PWM1模式输出极性高电平有效 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }调用参数上我传入了PSC71ARR999对应前面说的1kHz频率。要注意这里用的TIM_OC2Init对应于CH3和CH4吗不是OC1或OC2对应的是TIM3的CH1/CH2。我实现里实际上是把TIM3的CH3和CH4分别对应到PB0和PB1所以在配置OC时要选OC3和OC4。上面OC1只是示意实际工程里要写OC3和OC4的初始化这里提醒你注意对应关系。GPIO方向的初始化比较简单就不贴完整代码了核心就是把PA4到PA7都配置成推挽输出初始状态全部拉低保证上电瞬间电机不会偷跑。接下来是电机控制函数这是整个驱动最核心的部分。我实现了一个底层设置函数接收两个方向的逻辑值和两个PWM比较值然后一次性输出到引脚和定时器void Motor_Set(int left_direction, int left_speed, int right_direction, int right_speed) { // left_direction和right_direction取1表示正转0表示反转 if(left_direction 1) { GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_ResetBits(GPIOA, GPIO_Pin_5); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_4); GPIO_SetBits(GPIOA, GPIO_Pin_5); } if(right_direction 1) { GPIO_SetBits(GPIOA, GPIO_Pin_6); GPIO_ResetBits(GPIOA, GPIO_Pin_7); } else { GPIO_ResetBits(GPIOA, GPIO_Pin_6); GPIO_SetBits(GPIOA, GPIO_Pin_7); } // 通过修改TIM3的CCR寄存器来改变占空比 TIM_SetCompare3(TIM3, left_speed); TIM_SetCompare4(TIM3, right_speed); }在这个基础上前进后退拐弯的函数就是对这个底层设置函数的封装。比如前进就是左侧正转加右侧正转左转就是左侧速度减半右侧全速。这样封装的好处是任何新动作都只需要一次函数调用业务逻辑层非常干净。4.3 按键控制逻辑与实测效果我在主循环里用正点原子F103核心板上的三个按键做交互KEY1按下小车前进KEY0按下小车后退KEY_UP每按一次速度加一档。因为按键会抖动所以我复用了例程里的按键扫描函数检测到有效电平变化后延时消抖再确认。速度我用的是一个全局变量speed_level取值范围从200到900对应PWM比较值也就是占空比20%到90%。设置下限20%是有讲究的直流减速电机存在静摩擦力占空比太低时电机两端电压不足以克服静摩擦会出现“叫唤但不转”的现象。我给了一个200的初始值这样上电默认小车就能动而不是卡在0%的占空比上让新手以为程序写错了。实测效果上PWM占空比从200到900线性变化电机转速能明显感觉到从慢到快的平滑过渡没有阶梯感。转向方面40%差速比实现的转弯半径适中在普通桌面上就能测试不会瞬间冲出桌面。整个系统连续跑了十分钟电机微热L298N芯片有发热但手能按住属于正常范围。5. 常见问题与排查技巧5.1 电机不转/抖动的排查顺序小车接完线死活不转这是每个做驱动的人都会遇到的事。我的排查顺序是从电源开始往前查一步步缩小范围。先看L298N上的电源指示灯是否点亮再量12V输入和5V输出是否正常接着检查STM32和L298N是否共地然后用万用表量IN1、IN2引脚在程序运行后是否能正常翻转电平。如果电机是“嗡嗡叫但转子不动”大概率是PWM频率设置不合理或者占空比太低。前者把频率调到1kHz到10kHz之间即可后者把占空比抬高到20%以上。电机抖动还有一个隐蔽原因使能引脚ENA悬空。有些模块的ENA上有跳线帽如果你用PWM控制调速必须把跳线帽拔掉让PWM信号真正接入如果不拔模块内部把ENA拉到高电平你的PWM信号根本进不去转速永远是100%。5.2 PWM没反应或调速不线性PWM完全没反应先查定时器和引脚的对应关系。很多STM32的引脚有多重复用功能你配置的是TIM3_CH3输出但引脚的复用功能映射可能默认不是这个。F103的引脚不需要额外的AFIO重映射时通常默认就能用但如果你选错引脚比如用了PA6却以为它能输出TIM3_CH3那肯定没有波形。遇到这个问题我建议直接看一下芯片的数据手册中定时器通道和引脚对应的表格或者用示波器量一下引脚有没有波形这个问题两分钟就能定位。调速不线性是另一个常见问题。PWM占空比和电机转速并不是严格线性的关系因为电机内部的线圈电感、摩擦阻力、电源内阻都会影响实际转速。表现为占空比在30%到50%的时候转速变化不明显到了70%以上突然变快。这不是程序问题是电机本身的特性。要改善可以让PWM比较值按指数曲线映射而不是线性映射或者直接忽略这种非线性毕竟小车控制精度要求没那么高。5.3 L298N发热和电源问题的避坑L298N发热是很多人的心头病但这其实是一个正常现象。L298N用的是双极型晶体管工艺导通压降比MOSFET方案大得多电流越大发热越厉害。实测两路电机同时工作在1A电流下芯片表面温度到60度以上不奇怪。解决办法是加大散热片、降低PWM频率减少开关损耗或者干脆换MOSFET方案的驱动芯片。电源问题上面已经强调过再补一个实操经验不要用USB给F103核心板供电的同时指望核心板再通过GPIO输出引脚给L298N的逻辑部分供电。虽然L298N逻辑部分电流不大但如果你把外部传感器也接在核心板上总电流可能超过USB口的500mA极限导致核心板电压被拖垮、程序跑飞。正确的做法是电机电源、主控电源、逻辑电源各管各的只在信号地线上共地。最后再分享一个我实测中发现的细节上电顺序很关键。理想顺序是先给L298N供电再给STM32供电这样STM32初始化程序时能正确配置所有GPIO为低电平避免L298N在输入引脚悬空时误动作。如果反过来STM32先上电但程序还在跑初始化之前悬空引脚可能被内部上拉电阻拉到高电平导致电机在上电瞬间猛地动一下对机械结构不友好也容易吓人一跳。6. 实操心得与后续扩展整套驱动写下来我最想强调的一个经验就是先让硬件在最小范围内动起来再去堆软件功能。我第一次调试时直接写了全部功能结果电机不转排查了半天到最后发现是ENA跳线帽没拔浪费了一个晚上的时间。后来改成先把GPIO方向控制调通让电机能正转反转再加PWM调速让电机转速可控最后才加按键和转向逻辑。每加一个功能都验证一个问题出现时一下子就能定位到新加的环节。这套驱动写完之后后面可以扩展的方向很多。最简单的扩展是加一个蓝牙串口模块把按键控制改成手机控制代码只需要在业务逻辑层加一个串口解析函数。稍微复杂一点可以加编码器测速用TIM定时器的编码器模式读取电机转速做一个闭环PID调速这样小车走直线就不容易跑偏了。再往后加超声波模块做避障加红外传感器做循迹核心的电机驱动层代码几乎不用动这也是我为什么强调一开始就要把驱动层和控制逻辑层分离的原因。有同学问过我代码是用标准库还是HAL库。正点原子的老教程大量用的是标准库网上资料多、直接能搜到答案HAL库则更贴近STM32CubeMX的图形化配置流程代码生成效率高。两种库本身的控制原理完全一样都是操作寄存器。学的时候建议选一种主流库吃透不要频繁切换否则容易陷入工具选择的纠结中反而忽略了真正重要的电机控制原理本身。这篇驱动最核心的收获就是你亲手搞懂了“方向靠电平、速度靠PWM”这个电机控制的基本功这套概念可以平移到几乎所有电机驱动场景。哪怕以后用更高级的驱动芯片、更大功率的电机控制的本质都不会变。我的建议是拿到代码之后把每一部分和实物接线、芯片手册对照着看一遍改一改速度参数试试效果然后试着从零自己写一遍只有自己能写出来才算真的掌握。本文还有配套的精品资源点击获取