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引脚 |
|---|---|---|
| 左电机方向1 | IN1 | PA4 |
| 左电机方向2 | IN2 | PA5 |
| 左电机使能/调速 | ENA | PB0 (TIM3_CH3) |
| 右电机方向1 | IN3 | PA6 |
| 右电机方向2 | IN4 | PA7 |
| 右电机使能/调速 | ENB | PB1 (TIM3_CH4) |
| 电源地 | GND | GND |
电机供电我用的是一块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); }调用参数上我传入了PSC=71,ARR=999,对应前面说的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”这个电机控制的基本功,这套概念可以平移到几乎所有电机驱动场景。哪怕以后用更高级的驱动芯片、更大功率的电机,控制的本质都不会变。我的建议是拿到代码之后,把每一部分和实物接线、芯片手册对照着看一遍,改一改速度参数试试效果,然后试着从零自己写一遍,只有自己能写出来才算真的掌握。
本文还有配套的精品资源,点击获取
