news 2026/10/28 14:56:48

STM32+L298N直流电机驱动:接线、GPIO控制与PWM调速避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32+L298N直流电机驱动:接线、GPIO控制与PWM调速避坑

简介:面向STM32初学者的直流电机驱动实践资料,围绕STM32F103C8T6配合L298N双H桥芯片控制两路电机正转、反转与停止展开,适合练习GPIO推挽输出、按键扫描与驱动模块联调的嵌入式入门者参考。压缩包内共1个PDF文件,约6.97MB,以图文形式梳理接线与控制代码两部分:OUT1~OUT4接电机、IN1~IN4分别对应PA2~PA5,并强调L298N与开发板共地、5V端口悬空及升压供电等易错点;代码方面给出main.c主循环按键分支,以及motor.c、motor.h中停止、正转、反转的电平组合与宏定义写法。目前已有939人学习下载,另附独立12V供电、中断响应优化、编码器测速反馈与错误处理等改进建议,可帮助读者动手前理清引脚与代码的对应关系,规避电机不转、方向相反等常见问题。

1. 电机不转的时候,先别改代码

我拆过十几份新手交上来的 L298N 工程,最后定位到 C 语言写错的不到两份,其余全在接线和供电上:5V 端子被当成输入接了电源、GND 只连了一根、IN1 和 IN2 接反了还在反复调延时。这份资源属于 stm32 软件/插件 里很实在的一类——四份能直接拖进工程的源文件(main.c、motor.c、motor.h、key.c/key.h),配一张 L298N 到 PA2~PA5 的接线表,按键按下去就正转、反转、停止。

边界也清楚:STM32F103 系列 + L298N 双 H 桥 + 两个有刷直流电机,GPIO 推挽输出打高低电平定方向,不管调速、不做闭环。适合刚拿到开发板和驱动板、想让轮子先转起来的人,也适合会点灯但没搭过电机回路、不清楚共地怎么处理的人。

2. 接线与供电:IN1~IN4 落到 PA2~PA5,GND 只连一根就等着翻车

拿到这份代码的第一件事不是编译,是对着驱动板把线捋一遍。L298N 模块上有三组端子容易搞混:右侧的 OUT1/OUT2、OUT3/OUT4 是给电机的功率输出,左侧的 IN1~IN4 是逻辑输入,中间那一排是电源——12V、GND、5V。代码里写死了 IN1~IN4 接 PA2~PA5,所以接线顺序错了,代码怎么改都是白费。

2.1 一张接线表,把端子和引脚钉死

先把对应关系列清楚,照着插线,插完再核对一遍。这份资源里没有原理图,只有文字描述,我按实际模块的丝印整理成下面这张表,供电部分单独在后面一节讲。

L298N 端子接到哪里作用备注
OUT1 / OUT2电机 A 的两根线电机 A 的功率输出转向相反时对调这两根
OUT3 / OUT4电机 B 的两根线电机 B 的功率输出同上,别去改代码
IN1PA2电机 A 方向位 1推挽输出,3.3V 逻辑
IN2PA3电机 A 方向位 2同上
IN3PA4电机 B 方向位 1同上
IN4PA5电机 B 方向位 2同上
ENA / ENB保持板上跳线帽短接使能两路 H 桥取掉跳线帽才能接 PWM
12V外部电源正极电机功率输入常见 7~12V
GND电源负极 + STM32 的 GND公共参考地必须两点都连
5V不接板载稳压输出端当输入用会掉压、带不动

表的最后一行是这份资源里被强调最多的一条:5V 端子不要接。很多人看到模块上有个 5V,下意识拿它给单片机供电,或者反过来从单片机 5V 引过来给模块供电,结果就是电机哼哧哼哧转不动。原因在 2.3 节展开。

ENA 和 ENB 值得单独说一句。这两个是使能端,只要给高电平,对应的 H 桥才工作。模块出厂时上面插着跳线帽,等于把使能端直接拉到高电平,所以代码里根本不用管这两个引脚,上电就是全速使能。这也是为什么这份代码只有方向控制、没有速度控制——它不是没写,是硬件上就被跳线帽锁死了。

2.2 共地这件事,只连一根线等于没连

L298N 中间的 GND 要引出两条线:一条接外部电源的负极,一条接 STM32 开发板上的 GND。有人图省事只接了开发板那一头,外部电源的负极悬着,现象是电机偶尔动一下、方向乱跳、按键也跟着误触发。

逻辑电平和功率回路必须共享同一个参考点。STM32 输出 3.3V 高电平,L298N 判断“高”的依据是它自己的 GND 电位;如果两边的 GND 之间没有低阻通路,这个 3.3V 在驱动芯片看来可能是 1.8V、也可能是 4V,具体取决于两块板子电源之间的浮空电位差。表现出来就是玄学:手一碰就转,松开就停。

血泪经验是,共地线要单独走一根,不要指望排针上那根细细的杜邦线同时承担信号回流。电机启动瞬间电流能到几百毫安甚至上安,回流路径上那点线阻足够把地电位抬起来几十毫伏到几百毫伏,逻辑端就可能误判。我一般会把驱动板的 GND 和开发板的 GND 用一根短而粗的线单独连,信号线另外走。

还有一个细节:如果开发板是 USB 供电、驱动板是电池或适配器供电,两路电源的地本来是不通的,共地线就是唯一的连接点。这时候千万不要再去把 USB 的 5V 和驱动板的 5V 接到一起,等于把两个电源并联,轻则掉压重则烧稳压。

2.3 5V 端子为什么不能当输入,12V 又为什么会被“吃掉”一截

L298N 模块上那个 5V 端子,本质是板载稳压器的输出端,用来在输入 12V 的时候顺手给外部逻辑电路供 5V。它的电流能力有限,而且路径上有一堆压降。把它当输入用,等于让电流反向穿过稳压电路,电压先掉一截,再叠加 L298N 内部的饱和压降,电机端拿到的电压可能只剩三四伏。

L298N 是双 H 桥,每个桥臂由达林顿管构成,导通时不是理想开关。数据手册给的饱和压降在 1.4V 到 2V 之间(取决于电流),也就是说 12V 供电,电机两端实际拿到的可能只有 10V 出头。原资源里用升压板把 5V 升到 12V,这个做法能转,但升压板的输出电流普遍不大,电机空载还行,一堵转电压立刻塌下去。

常见的处理方式是:逻辑部分用开发板自己的 5V/3.3V,功率部分单独给一路 12V,两路只共地不共电源。手上只有 5V 的话,升压板要选输出电流大于电机堵转电流的规格,两个电机并联时按单个堵转电流的两倍去估,别按空载电流估。

2.4 上电顺序与空载自检

接线完成、还没插电机的时候,先做一次空载自检,这一步能省掉后面一半的排查时间。把电机线拔掉,只上逻辑电,让四个引脚按正转、停、反转、停循环,用万用表直流档量 L298N 的 OUT1 对 GND、OUT2 对 GND,看电平是不是跟着翻转。

/* 空载自检:不接电机,只观察 L298N 输出端电平变化 */ #include "motor.h" #include "delay.h" void Motor_SelfTest(void) { while (1) { Motor1_Set(MOTOR_FWD); /* OUT1 高、OUT2 低 */ Motor2_Set(MOTOR_FWD); delay_ms(1000); Motor1_Set(MOTOR_STOP); /* 两路输出同低,自由停车 */ Motor2_Set(MOTOR_STOP); delay_ms(500); Motor1_Set(MOTOR_REV); /* OUT1 低、OUT2 高 */ Motor2_Set(MOTOR_REV); delay_ms(1000); Motor1_Set(MOTOR_STOP); Motor2_Set(MOTOR_STOP); delay_ms(500); } }

这段函数用了第 3 章重构后的接口,逻辑很直白:正转一秒、停半秒、反转一秒、停半秒,循环。参数只有延时值,1 秒足够万用表读数稳定,如果你的表响应慢,把 1000 调到 2000。观察重点是 OUT1 和 OUT2 的电平是否互补——一个高一个低,切换时同时翻转。如果两个都是 0V 或者都是 12V 不动,问题在 GPIO 配置或者 IN 线没插实,跟电机没关系。

空载自检过了再插电机。插电机的时候先断电,别带电插拔,L298N 没有反接保护,插拔瞬间的感性反冲很容易送走一片。

上电顺序上,我一般先给 STM32 上电,确认程序跑起来了(比如板载 LED 在闪),再给驱动板上电。这样如果一上功率电就复位,能立刻判断是电源跌落而不是程序问题。

3. 驱动层代码:四个宏、三张真值表,把电机方向钉死

代码部分拆开看,其实就是两件事:把四个 GPIO 配成推挽输出,然后按真值表给高低电平。难点不在写,在于为什么这么写、以及原版那些看着别扭的地方要不要改。

3.1 motor.h 里的宏:用函数形状包装位操作

原版的写法是把 GPIO 操作塞进一个带 if 的宏:

#define IN1(a) if (a) \ GPIO_SetBits(GPIOA,GPIO_Pin_2);\ else \ GPIO_ResetBits(GPIOA,GPIO_Pin_2)

好处是调用处读起来像函数,IN1(High)就是拉高。问题有三个。一是宏展开后没有外层括号,遇到if (x) IN1(High); else ...这种上下文,else 会被吸进宏里,编译器不报错但行为错位。二是GPIO_SetBits和GPIO_ResetBits本身是函数调用,宏包一层并没有省多少事,反而让调试时看不到栈帧。三是宏名 IN1 直接暴露引脚,换引脚要改宏体,改完全工程重新编译。

我一般会改成下面这种形式,拆成置位和复位两个明确的宏,再把方向判断收敛到函数里:

/* motor.h:引脚映射集中在这里,换板子只改这八行 */ #ifndef __MOTOR_H #define __MOTOR_H #include "stm32f10x.h" #define IN1_H() GPIO_SetBits(GPIOA, GPIO_Pin_2) #define IN1_L() GPIO_ResetBits(GPIOA, GPIO_Pin_2) #define IN2_H() GPIO_SetBits(GPIOA, GPIO_Pin_3) #define IN2_L() GPIO_ResetBits(GPIOA, GPIO_Pin_3) #define IN3_H() GPIO_SetBits(GPIOA, GPIO_Pin_4) #define IN3_L() GPIO_ResetBits(GPIOA, GPIO_Pin_4) #define IN4_H() GPIO_SetBits(GPIOA, GPIO_Pin_5) #define IN4_L() GPIO_ResetBits(GPIOA, GPIO_Pin_5) typedef enum { MOTOR_STOP = 0, /* 两输出同低,自由停车 */ MOTOR_FWD, /* 正转 */ MOTOR_REV, /* 反转 */ MOTOR_BRAKE /* 两输出同高,短接制动 */ } Motor_Dir; void Motor_Init(void); void Motor1_Set(Motor_Dir d); void Motor2_Set(Motor_Dir d); void Motor_Both(Motor_Dir d); #endif

宏只做一件事:拉高或拉低某一个引脚,不再带条件分支,也就不存在悬垂 else。引脚编号集中在头文件,PA2 换成 PB6 只需要改四行。

3.2 Motor_Init:时钟、模式、初值三件事

初始化函数的套路是固定的三步:开时钟、填结构体、写初值。原版用了RCC_APB2Periph_GPIOA使能 GPIOA,GPIO_Mode_Out_PP推挽输出,GPIO_Speed_50MHz,最后把四个引脚全部拉低。这几个参数一个都不能想当然。

/* motor.c 初始化部分 */ #include "motor.h" void Motor_Init(void) { GPIO_InitTypeDef gpio; /* 第一步:开 GPIOA 时钟,不开时钟写寄存器无效 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* 第二步:四个方向脚统一配成推挽输出 */ gpio.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | 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_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5); }

参数逐个解释。GPIO_Mode_Out_PP是推挽输出,上下管都工作,能拉高也能拉低,驱动 L298N 的输入是标准做法;如果写成开漏Out_OD,高电平要靠外部上拉,没上拉就是悬空,L298N 会读成低,现象是永远只能反转或者不转。GPIO_Speed_50MHz影响的是输出翻转速率,电机控制这种毫秒级信号用 2MHz 都够,选 50MHz 是为了杜邦线拉长时边沿不至于太缓;速度档位越高功耗略大,但这里可以忽略。

第三步的初值容易被忽略。上电复位后 GPIO 默认是浮空输入,引脚状态不确定,如果 L298N 的使能已经打开(跳线帽短接着),浮空的 IN 引脚可能被干扰拉高,电机上电瞬间自己抽一下。初始化时立刻拉低,把这个窗口关掉。

3.3 三张真值表对齐:为什么 STOP 有两种写法

L298N 每路 H 桥有两个输入,组合起来就四种状态。原代码里 STOP 用的是两个都拉高,而初始化用的是两个都拉低,这两个其实都能让电机停,但停下来之后电机轴的手感不一样。

IN1IN2输出状态电机行为对应函数
10OUT1 高、OUT2 低正转MOTOR_FWD
01OUT1 低、OUT2 高反转MOTOR_REV
00两输出同低自由停车,轴可手转MOTOR_STOP
11两输出同高短接制动,轴转不动MOTOR_BRAKE

自由停车是把电机两端都接到低电平,线圈没有电流,转子靠惯性滑行;短接制动是把电机两端接到同一个高电平,线圈被短接,反电动势形成制动电流,停下来更快但冲击也更大。原代码的Motor_1_STOP用的是两个 High,属于短接制动,而初始化是全低,两处语义不一致,看代码的人容易以为自己漏了什么。

我的做法是把两种状态都显式定义出来,由调用方决定停的方式:

/* motor.c:方向控制,四种状态一一对应真值表 */ void Motor1_Set(Motor_Dir d) { switch (d) { case MOTOR_FWD: IN1_H(); IN2_L(); break; case MOTOR_REV: IN1_L(); IN2_H(); break; case MOTOR_BRAKE: IN1_H(); IN2_H(); break; default: IN1_L(); IN2_L(); break; /* MOTOR_STOP */ } } void Motor2_Set(Motor_Dir d) { switch (d) { case MOTOR_FWD: IN3_H(); IN4_L(); break; case MOTOR_REV: IN3_L(); IN4_H(); break; case MOTOR_BRAKE: IN3_H(); IN4_H(); break; default: IN3_L(); IN4_L(); break; } } /* 两个轮子一起动,做小车底盘时用得最多 */ void Motor_Both(Motor_Dir d) { Motor1_Set(d); Motor2_Set(d); }

switch 里的顺序和表格一一对应,看代码就能反查真值表。default 兜住 MOTOR_STOP,避免枚举漏项时输出不确定状态。

方向反了怎么办?这是最高频的提问。答案是不改代码,把电机上的两根线对调。因为“正转”是相对于电机轴而言的,两个电机一左一右装在底盘上,同样给 FWD,一个往前走一个往后走,这是机械安装决定的,代码里再取反只会让后面的人看不懂。真要统一,就在函数命名上区分左右轮,别在电平上做文章。

3.4 把两路电机绑成底盘动作

单独控制两个电机在实际项目里用得少,更多是“前进、后退、原地左转、原地右转”这种底盘动作。在Motor_Both之上再包一层,主循环里就干净了:

/* 底盘级动作:左右轮差速组合 */ void Chassis_Forward(void) { Motor_Both(MOTOR_FWD); } void Chassis_Backward(void) { Motor_Both(MOTOR_REV); } void Chassis_SpinLeft(void) /* 原地左转:左轮反转,右轮正转 */ { Motor1_Set(MOTOR_REV); Motor2_Set(MOTOR_FWD); } void Chassis_SpinRight(void) { Motor1_Set(MOTOR_FWD); Motor2_Set(MOTOR_REV); }

Chassis_SpinLeft这种差速组合是后面接遥控、接循迹的基础,现在把接口留出来,比等到主循环里堆一堆 if 再重构要省事。注意原地转向时两个电机同时反向,电流是叠加的,电源余量不够就会看到电压塌陷,这一点在第 5 章会展开。

4. 按键与主循环:KEY_Scan 的去抖逻辑,和那段被覆盖的 GPIO 初始化

按键部分看起来最简单,实际是这份代码里坑最多的。原版 key.c 有一行赋值写重了,导致两个按键根本没被配置成输入,很多人的“按键没反应”就出在这里。

4.1 上拉还是下拉,取决于按键另一端接哪

开发板上的按键接法通常有两种:一端接 GND,另一端接 GPIO,按下时引脚被拉到低,这种要配成上拉输入;一端接 VCC(3.3V),另一端接 GPIO,按下时被拉高,这种要配成下拉输入。配反了的现象是:不按的时候读到按下,按下去读到松开,或者干脆一直读到同一个值。

这份资源里 KEY0 接 PE4、KEY1 接 PE3,是接地型,配GPIO_Mode_IPU上拉;WK_UP 接 PA0,是接电源型,配GPIO_Mode_IPD下拉。上拉输入的内部电阻大概 30k~50k,按下时电流零点几毫安,够用且不会给按键增加负担。

4.2 那行被覆盖的 GPIO_Pin

原版 key.c 的初始化长这样:

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_3; // 想配 KEY0、KEY1 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // 这一行把它覆盖了 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOE, &GPIO_InitStructure);

结构体成员被连续赋值两次,第二次生效,所以 PE4 和 PE3 压根没进初始化,只配了 PE5。PE3/PE4 保持复位后的浮空输入状态,读回来的值取决于板子上有没有外部上拉电阻和当时的手摸环境。有些板子外部有 10k 上拉,看起来能凑合用;没有外部上拉的板子就是随机触发。这是“按键时灵时不灵”的典型来源。

修正后的版本把两次赋值合并,并且把结构体每个成员都赋到,不给残留值留机会:

/* key.c:按键初始化,注意 GPIO_Pin 只赋值一次 */ void KEY_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOE, ENABLE); /* KEY0=PE4、KEY1=PE3,按键另一端接地,按下为低,用上拉输入 */ gpio.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4; gpio.GPIO_Mode = GPIO_Mode_IPU; gpio.GPIO_Speed = GPIO_Speed_50MHz; /* 输入模式下该位无效,赋默认值防残留 */ GPIO_Init(GPIOE, &gpio); /* WK_UP=PA0,按键另一端接 3.3V,按下为高,用下拉输入 */ gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_IPD; GPIO_Init(GPIOA, &gpio); }

GPIO_Speed在输入模式下不参与硬件配置,但结构体是局部变量,如果不赋值就是栈上的随机值,虽然对输入无效,但排错时容易让人怀疑。显式赋值是个好习惯。

4.3 KEY_Scan 的去抖与优先级

原版的扫描函数逻辑是对的,静态标志加 10ms 延时去抖,支持单次和连续两种模式。有几处值得说清楚:

/* key.c:返回 0 表示无按键,1/2/3 分别对应 KEY0、KEY1、WK_UP */ u8 KEY_Scan(u8 mode) { static u8 key_up = 1; /* 松开标志,静态变量跨调用保持 */ if (mode) key_up = 1; /* mode=1 支持连按,每轮都允许触发 */ if (key_up && (KEY0 == 0 || KEY1 == 0 || WK_UP == 1)) { delay_ms(10); /* 去抖:等抖动窗口过去再读一次 */ key_up = 0; /* 标记已按下,松开前不再触发 */ if (KEY0 == 0) return KEY0_PRES; else if (KEY1 == 0) return KEY1_PRES; else if (WK_UP == 1) return WKUP_PRES; } else if (KEY0 == 1 && KEY1 == 1 && WK_UP == 0) { key_up = 1; /* 三个键都松开,恢复可触发状态 */ } return 0; }

static u8 key_up是这段代码的核心:它记住“上一次是不是已经处理过这次按下”,只有在按键松开后才复位。没有这个标志,按住不放会每轮循环返回一次键值,电机就会反复收到同一条命令。

delay_ms(10)是软件去抖,机械按键的抖动窗口一般在 5~20ms,10ms 是个折中值。如果按键质量差、抖动时间长,可以加到 20ms,代价是响应变慢,对电机控制这种场景无所谓。

优先级那段注释写得很明白:KEY0 > KEY1 > WK_UP。三个键同时按下时只有 KEY0 生效。正常使用不会同时按,但如果按键是并排的、手指按偏了,这个优先级决定了实际行为。要改的话,注意else if链的顺序和最后的松开判断要一起改,只改一半会导致某个键永远触发不了。

4.4 主循环:把“重复下发”改成“状态变化才下发”

原版 main.c 有两个地方值得改。一是上电后delay_ms(10000)等十秒,不知道是为了等电源稳定还是调试残留,实际用起来很难受;二是 case 分支里每个动作后面跟一句delay_ms(1),这个延时没有意义,去掉不影响任何行为。

更重要的是结构。原版的写法是“扫到键值就执行动作”,按键按住不放(mode=1)时会不停重新下发同样的命令,虽然对 GPIO 来说重复写同一个电平无害,但一旦后面加上 PWM 调速、加减速斜坡,重复下发就会把斜坡逻辑冲掉。

/* main.c:用当前状态 + 上次状态,只在变化时驱动一次 */ #include "stm32f10x.h" #include "motor.h" #include "key.h" #include "delay.h" typedef enum { CMD_NONE = 0, CMD_FWD, CMD_REV, CMD_STOP } Cmd; int main(void) { u8 key; Cmd cur = CMD_STOP, last = CMD_NONE; Motor_Init(); /* 先配电机引脚,避免上电抽动 */ KEY_Init(); delay_init(); while (1) { key = KEY_Scan(0); /* 0:单次触发,不连按 */ if (key == WKUP_PRES) cur = CMD_FWD; else if (key == KEY1_PRES) cur = CMD_REV; else if (key == KEY0_PRES) cur = CMD_STOP; if (cur != last) { /* 状态变了才真正下发 */ switch (cur) { case CMD_FWD: Motor_Both(MOTOR_FWD); break; case CMD_REV: Motor_Both(MOTOR_REV); break; default: Motor_Both(MOTOR_STOP); break; } last = cur; } } }

cur是“我想要的状态”,last是“上一次真正下发的状态”,两者不一致才动作。这样按住 WK_UP 十秒,电机也只是收到一次正转命令,不会每 10ms 重写一遍引脚。后面要接加减速时,只在这里加一个斜坡函数,不动按键逻辑。

初始化顺序也有讲究:Motor_Init()放在最前面,先把四个 IN 引脚钉在低电平,再去做按键和延时初始化,避免这中间的几十毫秒里引脚处于浮空状态。

5. 避坑与排查:五个现场,从电机不转到工程配置对不上

下面五条是这份资源在实际复现时最容易撞上的,按“现象 → 原因 → 解决”写。每条我都见过不止一次,尤其是第三条和第五条,光看代码看不出来。

5.1 电机完全不转,但 L298N 上的电源指示灯是亮的

现象:程序下载进去,按键按下有反应(比如串口能打印键值),电机一点动静没有,用手摸驱动板不烫,指示灯常亮。

原因:指示灯亮只说明模块的 12V 输入有电,不代表 H 桥被使能。最常见的两种情况,一是 ENA/ENB 的跳线帽被拔掉了(有人看到“可以调速”的教程就先拔了),使能端悬空,H 桥关断;二是共地线没接,逻辑电平没有参考点,IN 引脚上的 3.3V 在驱动芯片看来不到阈值。

解决:先看 ENA/ENB 跳线帽在不在,在的话用万用表量使能端对 GND 是不是高电平;再量 STM32 的 GND 和 L298N 的 GND 之间是不是通的,不通就补一根线。两步都过了还不转,把电机线拔下来,用一节 1.5V 电池直接碰电机两根线,看电机本身是不是坏的。

5.2 只有一个电机转,另一个纹丝不动

现象:按正转,左边轮子转,右边轮子不动;把两个电机的线对调,还是同一个位置不转,说明问题跟着通道走,不跟电机走。

原因:三个可能。IN3 或 IN4 的杜邦线没插实,或者插到了 PA4/PA5 之外的引脚;初始化时GPIO_Pin的位或写漏了一个(比如写成了GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4,漏掉 5);再或者是这一路 H 桥已经损坏,L298N 单路烧毁很常见,通常是电机堵转时间太长。

解决:先拔掉电机,空载量 OUT3、OUT4 的电平,按真值表逐项对;电平不对就顺着往回查 GPIO 配置和接线;电平对了但接上电机不转,换一个电机试,还不行就是 H 桥坏了,换模块。L298N 的散热片很小,长时间堵转必烧,调试阶段最好在电机回路里串一个限流或者别让它堵着。

5.3 按键时灵时不灵,手一靠近就自己触发

现象:有时候按下去没反应,有时候没按却自己转起来,冬天干燥的时候尤其严重。

原因:第 4 章说的那行被覆盖的GPIO_Pin赋值,PE3/PE4 没被配置成上拉输入,处于浮空状态,引脚电压由环境噪声决定,人体感应就足够让它翻转。

解决:把初始化改成 4.2 节的版本,确认GPIO_Pin只赋值一次、GPIO_Mode是GPIO_Mode_IPU。改完用万用表量 PE3、PE4 对 GND 的电压,不按时应该是 3.3V,按下时是 0V。如果板上本来就有外部上拉,还要检查焊接有没有虚焊。

5.4 电机一转,STM32 就复位或者程序跑飞

现象:按键触发后电机刚启动,板子重启,串口重新打印启动信息;或者程序卡死,按键全无响应。

原因:电源跌落。电机启动电流是额定电流的几倍,如果电机和 STM32 共用一路 5V(尤其是用升压板从 5V 升到 12V 再给电机),启动瞬间把 5V 拉到 3V 以下,MCU 的欠压复位就触发了。另一个来源是电机换向时产生的反电动势和尖峰通过地线串回逻辑部分。

解决:功率和逻辑分开供电,两路只共地;在电机两端并联一个 0.1uF 瓷片电容抑制换向火花,有条件再加一个续流二极管;共地线加粗缩短,别用最细的杜邦线;如果必须共用一路电源,在电机供电上串一个电感或者磁珠,并在 MCU 电源脚附近加 100uF 电解电容做储能。这几条都做完还复位,就要考虑升压板输出能力是不是根本不够,量一下电机启动瞬间的电压波形就清楚了。

5.5 代码能编译能下载,但行为跟预期对不上:型号和工程配置不匹配

现象:工程编译通过,下载后引脚行为不对,或者干脆进不了 main,卡在启动文件的死循环里。

原因:这份资源的标题写的是 STM32F103C8T6(48 脚、64KB Flash、中容量),正文里却提到了 F103ZET6(144 脚、512KB Flash、大容量)。这两个型号的启动文件、全局宏、Flash 容量都不一样:中容量要startup_stm32f10x_md.s加STM32F10X_MD,大容量要startup_stm32f10x_hd.s加STM32F10X_HD。用错了启动文件,中断向量表长度不对,程序跑起来就是随机行为。

解决:先确认手上板子的丝印型号,再去工程的 Options → C/C++ → Define 里核对宏定义,在工程文件列表里核对启动文件名,两者必须和型号容量匹配。PA2~PA5 这几个引脚在两个型号上都存在,所以代码本身不用改,改的是工程配置。另外 C8T6 只有 64KB Flash,工程里如果链了一堆用不上的库,很容易超容量,编译时报 region overflow 就是这个原因,把没用的外设库去掉即可。

6. 进阶:取下 ENA/ENB 跳线帽,用 TIM3 把开关控制换成 1kHz PWM

开关控制只能全速跑,实际做小车第一个想加的就是调速。改动点不在方向逻辑,在使能端:把 ENA、ENB 上的跳线帽取掉,把这两个端子接到定时器的 PWM 输出上,方向仍然由 IN1~IN4 决定,速度由占空比决定。

选 TIM3 的 CH1 和 CH2,对应 PA6 和 PA7,正好是 F103 的默认复用引脚,不用重映射。定时器时钟 72MHz,预分频 71 得到 1MHz 计数频率,自动重装值 999 得到 1kHz 的 PWM 频率。1kHz 在电机上会有轻微啸叫,但扭矩平稳;嫌吵可以改到 10kHz 以上,代价是低占空比时线性变差。

/* TIM3_CH1(PA6)->ENA,TIM3_CH2(PA7)->ENB,取掉板上跳线帽 */ void Motor_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); /* 复用推挽:引脚控制权交给定时器,不再由 GPIO 写 */ gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); tb.TIM_Period = arr; /* 999:占空比分辨率 1/1000 */ tb.TIM_Prescaler = psc; /* 71:72MHz/(71+1)=1MHz */ tb.TIM_ClockDivision = TIM_CKD_DIV1; tb.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &tb); TIM_OCStructInit(&oc); /* 先填默认值,避免残留 */ oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 0; /* 上电占空比 0,电机不转 */ oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &oc); TIM_OC2Init(TIM3, &oc); TIM_Cmd(TIM3, ENABLE); } /* duty 取值 0~1000,对应 0~100% 占空比 */ void Motor_SetSpeed(u16 duty) { if (duty > 1000) duty = 1000; TIM_SetCompare1(TIM3, duty); TIM_SetCompare2(TIM3, duty); }

三个参数值得记住:arr决定分辨率,999 意味着速度能分 1000 档,实际用 100 档就够;psc决定频率,改频率就改它;TIM_Pulse是初始占空比,一定给 0,否则上电电机就窜出去。GPIO_Mode_AF_PP不能写成普通推挽,否则定时器输出到不了引脚,占空比怎么调都是全速。

验证方法我一般走三步。第一步,不接电机,用示波器或者万用表直流档量 PA6 对 GND,Motor_SetSpeed(500)时应该读到约 1.65V(3.3V 的一半),改成 250 读到约 0.8V,说明 PWM 真的出去了。第二步,接上电机,从 0 往上慢慢加,找到能克服静摩擦起转的那个值,通常在两三百左右,这个值记下来作为后面的最小启动占空比。第三步,让电机从 200 加到 800 再降回 200,听声音、看转速是不是跟着连续变化,如果中间有跳变或者某一段没反应,多半是电源带不动,回到 5.4 节查供电。

调速加上之后,方向逻辑一行都不用改,Motor1_Set(MOTOR_REV)仍然是反转,只是反转的速度由占空比决定。这种分层是这份代码最有价值的地方:方向归方向,速度归速度,后面再加编码器闭环,只需要在Motor_SetSpeed前面插一个 PID 输出,其余部分不动。

从那以后我每次拿到这类“有代码”的电机工程,都强制走一遍固定流程:先空载量电平,再单电机试转,最后双电机上底盘,任何一步不过都不往下走。省下来的不是调试时间,是烧模块的钱。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 14:55:10

大模型学习路线:从基础理论到实战应用

1. 大模型学习路线全景解析作为一名从传统机器学习转型到大模型领域的技术从业者,我完整经历了从困惑到入门再到实战的全过程。大模型技术栈与传统AI开发有着显著差异,不仅需要掌握新的技术范式,还要理解分布式计算、参数高效微调等专属技能树…

作者头像 李华
网站建设 2026/10/28 14:54:47

混沌保密通信:同步收敛、掩码增益与DCSK误码率实践

简介:《混沌在保密通信系统中的应用研究》是一份面向信息安全、通信工程及相关专业学生与研究者的毕业论文文档,聚焦混沌理论在保密通信中的落地思路,重点解决混沌同步信号与明文信息同信道传输的问题。资源包共1个doc文件,约1.96…

作者头像 李华
网站建设 2026/10/28 14:54:42

产品增长停滞诊断与解决方案

1. 为什么产品增长会突然停滞?产品增长停滞是每个创业者和产品经理最害怕遇到的噩梦。上个月还保持着20%的月增长率,这个月突然就掉到了5%,甚至出现负增长。这种断崖式下跌往往让人措手不及,但更可怕的是找不到原因。我在运营Lenn…

作者头像 李华
网站建设 2026/10/28 14:54:00

LSTM-PID协同控制:面向工业过程的可解释时序预测与实时校正

简介:本资源是一份面向MATLAB开发者与智能控制研究者的LSTM-PID混合建模实战指南,聚焦时间序列预测与动态系统智能控制的融合创新,适用于工业自动化、智能电网、交通流量预测及医疗监护等非线性时变场景。资源以1个98KB的DOCX文档形式交付&am…

作者头像 李华
网站建设 2026/10/28 14:53:45

2026 Codes研发管理平台:数智化全流程与低门槛设计的创新实践

1. 项目概述:新一代研发管理平台的破局之道2026 Codes研发管理平台的出现,正在颠覆传统研发管理软件的市场格局。作为一名经历过十余个研发团队协作工具选型的资深技术管理者,我深刻理解当前研发管理领域的痛点:市场上充斥着大量功…

作者头像 李华
网站建设 2026/10/28 14:52:05

商业照明设计核心指标与光品质提升实践

1. 商业照明设计的关键认知升级十年前我刚入行时,商业照明还停留在"够亮就行"的粗放阶段。如今陪同客户验收项目时,他们开始拿着光谱仪测量显色指数,用照度计核对垂直面照度均匀度。这种变化背后是商业空间对光环境品质要求的指数级…

作者头像 李华