简介:面向STM32入门与电机驱动实践者的一份PDF文档,围绕STM32F103C8T6通过L298N双H桥控制两路直流电机的正转、反转与停止展开,适合正在做课程设计、智能小车底盘或GPIO外设练习的读者对照学习。压缩包仅含1个PDF文件,大小约6.97MB,篇幅集中,便于在单个文档内完成硬件连线与软件实现的连贯阅读。内容覆盖L298N输出端与两台电机的接法、IN1至IN4对应PA2至PA5的控制信号、共地处理及5V供电注意事项,并给出主程序按键扫描、电机GPIO初始化与正反转/停止函数的实现思路,还提示了升压板使用和独立12V供电等优化方向。已有939人学习,可作为快速理解H桥驱动逻辑、排查电机不转与方向异常问题的参考材料。
1. 从一次“电机只会嗡嗡响”的调试说起:STM32F103C8T6 + L298N 双电机按键控制的方案边界
第一次把 STM32F103C8T6 和 L298N 接在一起,卡住人的往往不是代码,而是 L298N 上那个标着 5V 的端子到底接不接。现象高度一致:程序烧进去,电机不转,只有一阵嗡嗡声,或者单片机跟着复位。这套方案解决的就是这个最小闭环——一块 STM32F103C8T6 最小系统板,经 L298N 双 H 桥驱动器带两台直流电机,用板上三个按键完成正转、反转、停止三种动作,代码拆成 motor、key、delay 三个模块,接线表、真值表和真机验收步骤给全。适合正在做电机驱动课程设计、需要把“按键控电机”这一节讲透的学生,也适合手上已有模块、想先把驱动跑通再往上加编码器和 PWM 的开发者。它不讨论 H 桥内部原理,只讲从接线、供电、代码到验收的一条可复现路径。
2. 系统总体设计:主控、驱动、电源、人机四部分的职责划分
一套能跑住的电机控制方案,先要把职责拆干净。主控只负责“决定什么时候输出什么电平”,驱动只负责“把弱电信号放大成能推动电机的电流”,电源只负责“别让前两者互相拖垮”,人机部分只负责“把人的意图翻译成一次函数调用”。这四块里任何一块越界,后面调试就是一团乱麻:最常见的越界是让开发板的 5V 去带电机,或者让主控的地和驱动的地各走各的。
2.1 主控选型与型号对不上的处理办法
STM32F103C8T6 是 48 脚的 LQFP 封装,64KB Flash、20KB SRAM,可用 IO 是 PA0–PA15、PB0–PB15、PC13–PC15,没有 GPIOE。这个细节值得单独拎出来讲,因为很多现成的电机例程是在 144 脚的大容量型号上写的,代码里大写特写GPIOE,直接搬过来编译能过,运行时按键却永远读到一个固定值——引脚物理上不存在,读回来的自然是 0。
这套逻辑本身对资源的要求极低:4 个推挽输出控驱动,3 个输入读按键,一个 SysTick 做延时,剩下 60 多 KB Flash 全空着。不需要 RTOS,不需要中断,裸机主循环完全够用。真正要花心思的是引脚分配,因为 C8T6 有几个默认被调试口占用的引脚,踩上去比没接按键更难查:
| 引脚 | 默认功能 | 拿来做按键的风险 |
|---|---|---|
| PA13 / PA14 | SWDIO / SWCLK | 改了会掉下载器,不建议动 |
| PA15 / PB3 / PB4 | JTDI / JTDO / NJTRST | 上电默认是 JTAG 复用功能,需关 JTAG 或重映射才当普通 IO |
| PA0 | WKUP,可做普通输入 | 可用,做下拉输入配高电平有效最省事 |
| PB5 / PB6 / PB7 | 普通 IO | 推荐把 KEY0、KEY1 挪到这里 |
| PA2 / PA3 | USART2 的 TX / RX | 做电机方向控制没问题,但要串口打印调试就得二选一 |
| PA4 / PA5 | SPI1 的 NSS / SCK | 不上 SPI 外设时可以放心占用 |
我一般会把电机方向线固定放在 PA2–PA5,原因是这四个脚连续、好记、没有调试口冲突,PCB 上也好走线;按键则挪到 PB5、PB6 加 PA0。改的时候只需要动key.h里那三行读引脚的宏和KEY_Init()里的时钟使能,其余逻辑一行不用改——这也是把按键读取封装成宏的价值。
2.2 电气拓扑与信号流向
整套系统的电源和信号是两条独立的路径,混在一起就出事。功率路径是:外部电源正极进 L298N 的 +12V 端子,负极进 GND 端子,L298N 内部两路 H 桥把电流送到 OUT1/OUT2(电机 A)和 OUT3/OUT4(电机 B)。信号路径是:STM32 的 PA2–PA5 输出 3.3V 逻辑电平到 IN1–IN4,L298N 内部比较后决定桥臂的开关状态。两条路径唯一的交点是地——L298N 的 GND 必须和 STM32 的 GND 连起来,否则 3.3V 的高电平在驱动器眼里没有参考点,等于没给信号。
这里有个反直觉的地方:L298N 模块上的 +5V 端子是输出,不是输入。模块板上有一颗 78M05 线性稳压,把 +12V 降压到 5V,一路供给 L298N 自己的逻辑电路,一路从 5V 端子引出来,原本是设计给单片机供电的。如果这个端子又接了一路外部 5V,两路电源就会顶在一起,逻辑电平被拉偏,表现出来就是电机转得极慢甚至只嗡鸣。原始方案里那句“5V 端口不接,不然很难转动”,本质就是这个冲突。
提示:模块上通常还有一个三针跳线帽盖在 5V 使能位置。短接时用板载稳压给逻辑供电,此时 5V 端子悬空;拔掉跳线帽后板载稳压断开,需要外部从 5V 端子输入 5V 逻辑电源。两种模式只能选一种,不能同时成立。
2.3 软件分层:应用层、控制层与板级支持
代码按三层切,后面扩展才不至于推倒重来。板级支持层是delay.c和key.c,一个提供毫秒延时,一个把引脚电平翻译成键值;控制层是motor.c与motor.h,对外只暴露 7 个函数——1 个初始化加 6 个动作,调用者完全不需要知道 IN1 到底接的是 PA2 还是 PB5;应用层是main.c,只做一件事:拿到键值,决定调哪个动作函数。
这样分的好处很实际。想换成 PWM 调速,只改控制层内部,把Motor_1_PRun()里的电平写死改成“方向脚写死 + 使能脚给占空比”,应用层一行不动;想换成串口指令控制,只改应用层的输入源,控制层照样不动。很多课程设计做到后期想加功能时发现改不动,根子都在一开始把引脚操作、按键判断和业务逻辑全堆在main.c里了。
3. 硬件连接落地:从 OUT 端子到共地线的完整接线表
硬件这一章值得写细,因为这套方案 90% 的“不转”都发生在接线环节,而接线错误又不报错,只能靠表和表笔一点点核。
3.1 接线表与引脚映射
按下面这张表接,接完再上电,不要边接边试。
| L298N 端子 | 接到哪里 | 说明 |
|---|---|---|
| OUT1、OUT2 | 电机 A 的两根线 | 不分正负,方向不对就互换这两根 |
| OUT3、OUT4 | 电机 B 的两根线 | 同上 |
| IN1 | PA2 | 电机 A 方向控制位 1 |
| IN2 | PA3 | 电机 A 方向控制位 2 |
| IN3 | PA4 | 电机 B 方向控制位 1 |
| IN4 | PA5 | 电机 B 方向控制位 2 |
| +12V | 外部电源正极 | 电机功率输入,建议 9–12V |
| GND | 外部电源负极 + STM32 的 GND | 两个 GND 必须连,且用同一根线就近连 |
| +5V | 悬空 | 板载稳压输出,不外接 |
| ENA / ENB | 保持跳线帽短接 | 短接即满速使能;拔掉需外部给高电平 |
关于“方向反了”:如果按下正转键电机往反方向转,优先交换同一路电机的两根输出线,不要去改代码里的电平组合。改线是物理事实的修正,改代码是把错误固化进逻辑里,后面加第二台电机时会乱。要真想从软件侧调,也应该改控制层里某个统一的宏或常量,而不是散在各个函数里。
还有一个细节容易被忽略:L298N 的逻辑高电平门限最低约 2.3V,STM32 的 3.3V 输出是够的,但余量不算大。如果模块板上带了光耦或者电平转换,或者开发板本身供电偏低到 3.0V 附近,就可能出现“偶尔不响应”的玄学现象。遇到这种时好时坏的情况,先用万用表量一下 PA2 输出高电平时的实际电压,再量 L298N 的 IN1 端子上收到的电压,压降超过 0.5V 就说明中间有接触电阻或者转换电路在拖后腿。
3.2 电源方案:升压板、独立电源与电流余量
原始方案里用升压板把 5V 抬到 12V,这是手上只有 5V 电源时的务实做法,但必须清楚它的边界。普通直流减速电机的堵转电流可以到 1–2A,两台同时启动的瞬时电流更陡,而常见的 5V 升 12V 小板标称 2A,实际持续输出往往要打折,效率也就 85% 上下。电流不足的直接表现不是“转得慢”,而是升压板输出电压被拉塌,STM32 跟着掉电复位,看起来像程序跑飞。
| 供电方式 | 适用场景 | 注意事项 |
|---|---|---|
| 5V 升 12V 升压板 | 临时验证、手上只有 USB 电源 | 输入端要能提供 3A 以上,输出端并 470μF 以上电解 |
| 12V 直流适配器 | 桌面调试、长时间运行 | 选 2A 以上,注意极性 |
| 电池组(如 3 节 18650 串) | 需要移动的演示 | 注意过放保护,接线端子要能过大电流 |
| 开发板 5V 直接带电机 | 不建议 | 电流远超板载稳压能力,必复位 |
L298N 内部是双 H 桥,每路导通时有两个三极管的饱和压降,合计约 1.4–2V。也就是说 12V 输入,电机两端实际只拿到 10V 出头。这是器件特性,不是接线问题,看到“电压怎么少了 2V”不用怀疑人生。真正要担心的是电流够不够、纹波大不大。
抑制纹波最省事的两招:电机两端各并一只 0.1μF 瓷片电容,滤掉换向时的高频尖峰;电源入口并一只 470–1000μF 电解电容,给瞬时大电流做个缓冲。这两只电容加起来不到一块钱,能省掉后面大半的“玄学死机”。
3.3 上电顺序与静态自检
接线完成后不要立刻烧程序,先做一遍不带电和带电的静态检查,顺序如下。
第一步,断电状态下用万用表通断档量 STM32 的 GND 与 L298N 的 GND 是否导通,这是整套系统能不能通信的前提。第二步,把电机从 OUT 端子上摘掉,只上电不给信号,量 L298N 的 +5V 端子对 GND 是不是 5V 左右,正常说明板载稳压在工作。第三步,烧一个只做 GPIO 翻转的空程序,让 PA2–PA5 轮流输出高低,用万用表或示波器逐脚确认电平确实在变。三步都过,再接电机上电。
注意:上电顺序建议先给主控、再给驱动功率,断电时反过来。这不是讲究,是为了避免驱动器先上电时把输出脚拉成未知状态,让电机“自己抖一下”。
4. GPIO 与电机控制层实现:真值表、宏封装与主循环改造
代码部分的核心不是写出能转的程序,而是写出改得动、查得快的程序。下面从真值表开始,一路讲到把原始主循环改成状态机。
4.1 H 桥真值表与“停止”的两种含义
L298N 每一路 H 桥有两个输入脚,它们的组合决定输出端的电位,进而决定电机状态。下面这张表是控制层全部逻辑的来源,务必记牢:
| IN1 | IN2 | OUT1/OUT2 状态 | 电机表现 |
|---|---|---|---|
| 1 | 0 | OUT1 高、OUT2 低 | 正转 |
| 0 | 1 | OUT1 低、OUT2 高 | 反转 |
| 0 | 0 | 两路都低 | 自由停车,靠惯性滑行 |
| 1 | 1 | 两路都高 | 制动刹车,绕组被短接,快速停下 |
原始代码里的Motor_1_STOP()用的是 IN1=1、IN2=1,也就是第二行最后那种“刹车”模式。这在演示场景下很合适,按键一松电机立刻停住,视觉上干净。但要清楚它和“断电滑行”不是一回事:刹车模式下电机绕组处于短路状态,会有明显的反向制动感,长时间保持这个状态对驱动芯片的发热也不友好。如果电机需要带动惯性较大的负载,或者你想让它自然减速,应该改成两个都输出低。
另外,当 ENA/ENB 被拉低时,无论 IN 脚什么组合,输出都是关断的。这一点在扩展 PWM 调速时就是入口:方向脚保持固定,使能脚给 PWM,占空比就是平均电压。
4.2 motor.c 与 motor.h 的实现细节
初始化部分的工作很固定:开 GPIOA 时钟、把 PA2–PA5 配成推挽输出、初始拉低。推挽输出是必须的,开漏输出没有上拉就驱动不了 L298N 的输入。速度等级上,原版给的是 50MHz,实际这个场景 2MHz 完全够用,输出边沿缓一点反而对 EMI 友好。
#include "motor.h" #include "stm32f10x.h" #include "stm32f10x_rcc.h" /* 电机方向控制脚初始化:PA2~PA5 推挽输出,初始低电平 */ void Motor_12_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* 开 GPIOA 时钟 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; /* 推挽输出 */ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; /* 降速减小干扰 */ GPIO_Init(GPIOA, &GPIO_InitStructure); /* 上电先全部拉低,避免驱动端出现未知电平 */ GPIO_ResetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5); }原始头文件里用宏封装了四个引脚操作,写法是#define IN1(a) if (a) GPIO_SetBits(...); else GPIO_ResetBits(...)。这种多行宏在单独调用时没问题,但一旦嵌进带 else 的语句里就会出岔子:宏展开后的if/else会和外层else配对错,编译器不报错,行为却完全不对。这类问题查起来极痛苦,因为代码看起来毫无问题。
稳妥的做法是换成static inline函数,编译器同样会内联,还能做类型检查,调试时能单步进去:
/* motor.h 中的引脚操作改成内联函数,避免多行宏的悬垂 else 问题 */ static inline void IN1_Set(uint8_t level) { if (level) GPIO_SetBits(GPIOA, GPIO_Pin_2); else GPIO_ResetBits(GPIOA, GPIO_Pin_2); } /* IN2_Set / IN3_Set / IN4_Set 同构,此处略 */ /* 控制层对外接口:调用者不关心引脚编号 */ void Motor_1_PRun(void) { IN1_Set(0); IN2_Set(1); } /* 正转 */ void Motor_1_NRun(void) { IN1_Set(1); IN2_Set(0); } /* 反转 */ void Motor_1_STOP(void) { IN1_Set(1); IN2_Set(1); } /* 刹车 */ void Motor_1_Free(void) { IN1_Set(0); IN2_Set(0); } /* 自由滑行 */参数说明上,Motor_12_Config()必须在任何动作函数之前调用一次;几个动作函数没有返回值,重复调用同一动作不会累积副作用,这一点在状态机里很重要——可以放心地每个周期都刷一次。
4.3 按键扫描与主循环改造
按键部分的关键是消抖和“一次按下只触发一次”。KEY_Scan()用一个静态变量key_up记录按键是否已经松开,按下时先等 10ms 消抖再确认,返回后把key_up清零,直到三个键全部松开才允许下一次触发。mode参数置 1 时每次都重置key_up,就变成支持长按连续触发。这三个键的优先级是固定的:KEY0 高于 KEY1,KEY1 高于 WK_UP,同时按下只有最高优先级的生效。
WK_UP 是下拉输入配高电平有效,KEY0、KEY1 是上拉输入配低电平有效,两种配置在同一个函数里混着用,读的时候很容易写反。如果发现“不按也触发”,先查这里。
原始主循环有两处可以改。一是开头delay_ms(10000),上电干等 10 秒,演示时很尴尬;二是每个 case 里的delay_ms(1),没有实际作用,只是让人以为在防抖。更实用的做法是引入一个状态变量,只在状态发生变化的瞬间执行动作,其余时间空转:
typedef enum { MOTOR_STOP = 0, MOTOR_FWD, MOTOR_REV } MotorState; int main(void) { uint8_t key; MotorState cur = MOTOR_STOP, last = MOTOR_STOP; Motor_12_Config(); KEY_Init(); delay_init(); while (1) { key = KEY_Scan(0); /* 无按键时返回 0 */ if (key == WKUP_PRES) cur = MOTOR_FWD; else if (key == KEY1_PRES) cur = MOTOR_REV; else if (key == KEY0_PRES) cur = MOTOR_STOP; if (cur != last) /* 只在状态切换时动作 */ { switch (cur) { case MOTOR_FWD: Motor_1_PRun(); Motor_2_PRun(); break; case MOTOR_REV: Motor_1_NRun(); Motor_2_NRun(); break; default: Motor_1_STOP(); Motor_2_STOP(); break; } last = cur; } delay_ms(10); /* 主循环节拍,别用空转占满 CPU */ } }改成状态机后,往里面加东西就顺手了。想加一个“点动”键,加一个枚举值和一个 case 即可;想加限位开关,在切换前查一下输入电平即可;想加速度斜坡,在切换时启动一个定时器逐步改占空比即可。
5. 避坑与常见问题:五条现场最容易翻车的点
下面这几条,几乎每一条我都在不同场合见过至少一次。按“现象、原因、解决”写,对照着排查比逐行读代码快得多。
5.1 电机一转,单片机就复位或程序跑飞
现象:没按键时一切正常,一按正转键电机刚起步,串口就断、程序从头开始跑,或者干脆卡死。
原因:功率回路把主控拖垮了。最常见的是电机电流从开发板的 5V 取,板载稳压瞬间过流;其次是升压板输出能力不足,电机启动瞬间把 12V 拉到 8V 以下,连带逻辑电源塌陷;第三是共地线太细或者根本没接,回流路径经过信号地,把地电位抬起来了。
解决:把电机功率和主控供电彻底分开,各用各的电源;两根地线在电源负极处单点汇合,线径加粗;电源入口并 470μF 以上电解电容、电机两端并 0.1μF 瓷片电容;如果还不稳,在 STM32 与 L298N 之间加光耦或者至少加 100Ω 串阻,做一次信号隔离。
5.2 电机不转只有嗡鸣,或者转得极慢
现象:程序确实在跑,电平也在变,但电机只有微弱振动或者转速肉眼可见地慢。
原因:按概率排,第一位是 L298N 的 5V 端子接了外部电源,和板载稳压打架;第二位是 ENA/ENB 跳线帽被拔掉却没给使能信号,输出被关断;第三位是升压板带不动,输出电压塌了;第四位是电机线只接了一根或者接触不良,单边绕组通电。
解决:先拔掉 5V 端子上的线,只留 +12V 和 GND;确认 ENA/ENB 要么短接跳线帽,要么用杜邦线接到 5V;用万用表量 +12V 端子在电机启动瞬间的电压,跌超过 2V 就说明电源不行;最后量 OUT1、OUT2 对 GND 的电压,正转时应该一个接近 12V、一个接近 0V,两端都是 0 或者都是 12V 就说明信号没到位。
5.3 按键按了没反应,或者一上电就自己触发
现象:按键完全无响应,或者手还没碰,程序已经执行了一次动作。
原因:第一种情况多是把按键接到了 GPIOE——C8T6 上根本没有这个端口,读到的是固定值;或者接到了 PB3、PB4 这类默认 JTAG 复用的脚上。第二种情况是上下拉配反了:WK_UP 是下拉输入配高电平有效,KEY0、KEY1 是上拉输入配低电平有效,混用的时候宏定义写反就会一上电就“以为按下了”。
解决:先用万用表确认按键实际连到哪个物理引脚,再对照芯片手册确认这个脚有没有默认复用功能;把key.h里的读引脚宏和KEY_Init()里的时钟、引脚号、上下拉配置三处对齐;实在查不出,写个只读引脚并打印的小程序,按下和不按各打一次,看电平变化是否和预期一致。
5.4 两台电机转速差很多,一台明显带不动
现象:同样的指令,电机 A 转得欢,电机 B 慢半拍甚至要用手拨一下才起转。
原因:两台电机本身的一致性差异(同型号也会有 10% 以上的参数离散)、其中一路 H 桥的压降更大、或者对应的 IN 脚接触电阻偏大导致逻辑电平偏低。
解决:先把两台电机的线互换,看问题跟电机走还是跟通道走——跟电机走就是电机本身的问题,跟通道走就查那一路的接线和焊点。软件上可以做补偿,在 PWM 阶段给弱的那一路略高的占空比,但这是最后一步,先排除硬件。
5.5 停止后电机还在转一会儿,或者停得太猛
现象:按停止键,电机不是立刻停,而是滑行一段;或者反过来,停得太急,整个支架都在抖。
原因:这是 4.1 里说的停车方式差异。IN1=IN2=1是刹车,绕组短接,制动力强;IN1=IN2=0是自由停车,靠惯性滑行。原始代码用的是刹车,但如果你的接线或代码改过,很可能落到了滑行模式。
解决:想停得干脆就用 1/1,想停得平顺就用 0/0;带减速箱、负载惯量大的场合建议刹车,纯演示、轻负载可以用滑行。答辩时被问到“停止是怎么实现的”,能把这两种模式的电气差异讲清楚,是个加分项。
6. 验收与答辩:用一张测试表证明这套驱动真的可控
代码烧进去能转,不等于方案做完了。验收要能回答一个具体问题:给我任意一个按键,我能不能预测电机会做什么,并且当场演示出来。我一般会准备下面这张表,逐项打勾,全部通过才算收工。
| 测试项 | 操作方法 | 通过标准 |
|---|---|---|
| 静态电平 | 不按任何键,量 PA2–PA5 | 全部为低或全部为高,无中间态抖动 |
| 单路正转 | 按 WK_UP | 两台电机同向转,方向与设计一致 |
| 单路反转 | 按 KEY1 | 两台电机反向转,转速与正转接近 |
| 停止 | 按 KEY0 | 电机在 1 秒内停住,无持续嗡鸣 |
| 重复触发 | 同一键连按 10 次 | 每次动作干净,无卡死、无漏触发 |
| 长时运行 | 连续正反转交替 5 分钟 | 驱动芯片温升可接受,主控不复位 |
| 断电恢复 | 断电再上电 | 上电后处于停止状态,不出现自启动 |
这张表的价值在答辩现场特别明显。评委问“你怎么保证它稳定”,你不用讲一堆形容词,把表拿出来,说这一项怎么测的、结果是什么、哪一项一开始没过后来怎么改的。真实的调试痕迹比任何漂亮话都有说服力。
答辩时最容易被追问的三个点,提前准备好答案。第一是“为什么 IN1=1、IN2=1 是停止而不是短路”——要能说清这是绕组短接制动,电流在 H 桥内部形成回路,电机反电动势被消耗掉,所以停得快。第二是“为什么必须共地”——逻辑电平是相对地而言的,没有共同参考点,3.3V 在驱动器眼里什么都不是。第三是“如果我要调速怎么改”——把 ENA/ENB 的跳线帽拔掉,接到带定时器的引脚上输出 PWM,方向脚不变,控制层加一个设置占空比的函数,应用层完全不用动。这三个问题答得出来,说明你是真把方案想通了,而不是照着例程抄了一遍。
我自己的血泪经验是:这套东西最花时间的从来不是写代码,而是第一次上电前那半小时的静态检查。早年我图快,接完线直接上电,结果电机没转、芯片发烫,查了两个晚上才发现是 5V 端子和外部电源顶在了一起。从那以后我每次接完功率回路,都强制走一遍“断电量通断、空载量电压、空开发板量电平”这三步,一次没落下过。希望帮到你。
本文还有配套的精品资源,点击获取