简介:面向STM32嵌入式入门、课程设计与小型DIY场景,这份PDF文档围绕STM32F103C8T6通过L298N驱动两个直流电机实现正转、反转和停止展开,适合已掌握基础GPIO与C语言、希望打通主控到执行器链路的读者。内容从器件组成与硬件连线讲起,说明L298N的OUT1至OUT4接电机、IN1至IN4接PA2至PA5,以及共地、12V供电和5V端口不接等易错点,并给出main.c、motor.c、motor.h与key.c的按键控制代码及电机状态宏定义。压缩包共1个PDF文件,大小约6.97MB,便于集中查阅接线说明与程序片段。已有939人学习,文档还附电源管理、中断响应、编码器反馈和错误处理等优化方向,可帮助读者快速复现实验并排查电机不转、方向相反等问题。
1. 需求先立住:两个直流电机的正反转,最小闭环该怎么搭
小车底盘、简易云台、传送带样机,最先卡住的往往不是算法,而是“两个轮子怎么让它听话地正转、反转、停住”。我见过不少人一上来就上编码器、上 PID,结果连电机为什么不转都没查明白。这套 STM32F103C8T6 控制 L298N 驱动两路直流电机的方案,干的就是把最小闭环先跑通这件事:三个按键对应正转、反转、停止,四根 GPIO 决定两个电机的转向,代码拆成电机模块和按键模块,接完线烧进去就能动。它适合正在做嵌入式课设、毕设前期动作验证,或者要给某个机械结构配一套最简驱动逻辑的人——先把“能动能停”钉死,再谈调速和闭环。
2. 硬件连接设计:L298N 四根信号线与两路电源的接法
这套东西翻车的概率,八成不在代码上,在线上。L298N 这块板子便宜、耐造,但它把“逻辑电源”和“电机电源”摆在同一排端子上,新手很容易把 5V 端当成“给板子供电的入口”接上去,然后得到一台嗡嗡响却转不动的电机。下面按“电机侧—信号侧—电源侧”的顺序拆。
2.1 电机侧与信号侧的引脚对应
L298N 是双 H 桥,一路 H 桥管一个电机。OUT1、OUT2 是一组,接第一个电机;OUT3、OUT4 是另一组,接第二个电机。这两个输出是直接给电机绕组供电的,接线没有极性讲究——因为极性反了只是转向反,把接在电机上的两根线对调一下就正过来了。我一般会在装车之前先用杜邦线临时接,跑通了再焊,省得拆。
信号侧四根线是控制的全部入口:IN1、IN2 管第一路,IN3、IN4 管第二路。原料里把它们分别落在 PA2、PA3、PA4、PA5,这四根都在 GPIOA 上,属于同一组时钟,初始化时一次RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)就够,不用开两个外设时钟,代码干净。
需要留意型号差异:原始工程里出现过 F103ZET6 的字样,而标题是 C8T6。两者内核和外设编程模型一致,标准外设库代码可以直接搬,差别在引脚数量——C8T6 是 48 脚,只有 PA、PB、PC 的部分引脚,没有 GPIOE。所以照着 ZET6 的工程往 C8T6 上移植时,凡是落在 PE 口的按键、LED 都要重新分配,这一点在第 5 章会展开。
2.2 12V 主供、5V 升压与共地这条命脉
L298N 的电源端子有三个位置需要说清楚:12V 输入(电机电源)、GND、5V 输出。
12V 端子接外部电源正极,这一路是喂给电机的,电压高低直接决定转速和力矩。GND 是公共地,必须引出两条线:一条接开发板的 GND,一条接外部电源的负极。只接一条都不行——L298N 和 MCU 之间传的是电平信号,电平是相对于各自的地来判断的,地不共,PA2 输出 3.3V,L298N 那边看到的可能是“说不清是多少伏”,表现就是时转时不转、按了没反应,甚至一上电就乱转。这类问题排查起来非常耗时间,属于典型的“血泪经验”。
5V 端子不接,这是原料里特别强调的一点,也是很多人踩的坑。L298N 板上自带 78 系列稳压,从 12V 降压产出 5V,这个端子是输出,用来给 MCU 供电的(板上通常有个跳线帽控制是否启用)。如果你在外面又用升压板把 5V 升到 12V 送进 12V 端子,同时又从别处把 5V 灌进这个 5V 端子,两路电源顶在一起,逻辑侧电压被拉偏,电机就转得很勉强甚至不转。手上只有 5V 电源(充电宝、USB)的时候,正确做法是:5V 经升压板升到 12V,进 L298N 的 12V 端;开发板自己用 USB 或另一路 5V 供电;两块板子的 GND 连到一起。升压板的输出电流要留余量,两个小直流电机空载几百毫安,堵转时能到一两安,选个标称 2A 以上的模块更稳。
2.3 上电顺序和一张接线自查表
上电顺序也有讲究:先接好所有线(包括地线),再给逻辑侧上电,最后给电机电源上电。反过来操作的话,MCU 还没初始化,PA2~PA5 处于复位后的浮空输入状态,L298N 的输入端悬空,容易被干扰电平触发,你会看到电机“抽搐一下”。虽然不致命,但调试时会让人怀疑代码。
下面这张表我一般贴在工位上,接完线逐行对一遍:
| 序号 | 从 | 到 | 说明 |
|---|---|---|---|
| 1 | L298N OUT1/OUT2 | 电机 A 两根线 | 转向反了对调这两根 |
| 2 | L298N OUT3/OUT4 | 电机 B 两根线 | 同上 |
| 3 | L298N IN1 | PA2 | 电机 A 方向位 1 |
| 4 | L298N IN2 | PA3 | 电机 A 方向位 2 |
| 5 | L298N IN3 | PA4 | 电机 B 方向位 1 |
| 6 | L298N IN4 | PA5 | 电机 B 方向位 2 |
| 7 | L298N 12V | 升压板输出正 | 电机电源 |
| 8 | L298N GND | 升压板输出负 | 功率地 |
| 9 | L298N GND | 开发板 GND | 信号地,必接 |
| 10 | L298N 5V | 悬空 | 不接,接了容易转不动 |
接线本身不复杂,复杂的是“看起来都接对了但就是不转”。这张表的价值在于把“看”变成“对”,逐条打勾,能省掉大量瞎猜。
3. 控制逻辑:从 H 桥真值表推到四个 GPIO 的电平约定
L298N 内部是两组 H 桥,每个桥臂两个三极管(现在多是 MOS 或复合管),对角导通时电流从左到右流过电机,另一组对角导通时电流反向。所以控制一个电机只需要两个逻辑位,四种组合,对应四种状态。把这四种状态想明白,代码基本就是抄表。
3.1 L298N 输入电平与电机状态的四种组合
以电机 A 为例,IN1、IN2 的四种组合:
| IN1 | IN2 | H 桥状态 | 电机表现 |
|---|---|---|---|
| 0 | 1 | 一对角导通 | 正转 |
| 1 | 0 | 另一对角导通 | 反转 |
| 0 | 0 | 全部截止 | 自由停车(惯性滑行) |
| 1 | 1 | 下桥臂同时导通 | 刹车(快速制动) |
原料代码里用的是:正转IN1(Low); IN2(High);,反转IN1(High); IN2(Low);,停止IN1(High); IN2(High);——也就是选择了“双高刹车”。这里没有对错,只有场景差别,下一节展开。需要提醒的是,正转/反转的“正”是相对于 OUT 接线定义的,你代码里叫正转,装到车上可能往后跑,那就把电机那两根线对调,别去改代码——改代码会让后面的逻辑全反,越改越乱。
3.2 引脚分配与宏封装的取舍
原料用的是宏封装,把“置高/置低”压成一个IN1(a)这样的调用,好处是动作函数里读起来像伪代码:
#define High 1 #define Low 0 /* 原始写法:if-else 直接展开 */ #define IN1(a) if (a) \ GPIO_SetBits(GPIOA, GPIO_Pin_2); \ else \ GPIO_ResetBits(GPIOA, GPIO_Pin_2)这种写法在简单调用时没问题,但有两个隐患。第一是宏展开没有作用域,if (a) IN1(x); else ...这种嵌套里,宏内部的else会跟外层else配对错位(悬垂 else),编译能过,运行时行为却是错的,这类 bug 极难定位。第二是没有类型检查,IN1(2)也能编过,实际等价于置高,属于埋雷。通常的做法是包一层do { } while (0),或者干脆写成static inline函数:
/* 推荐写法一:do-while(0) 包住,保证宏是一个语句块 */ #define IN1(a) do { \ if (a) GPIO_SetBits(GPIOA, GPIO_Pin_2); \ else GPIO_ResetBits(GPIOA, GPIO_Pin_2); \ } while (0) /* 推荐写法二:内联函数,有类型检查,Keil 下加 __inline */ static __inline void IN1(uint8_t level) { if (level) GPIO_SetBits(GPIOA, GPIO_Pin_2); else GPIO_ResetBits(GPIOA, GPIO_Pin_2); }do { } while (0)的作用是让整个宏在语法上等价于一条语句,后面加分号不报错,也不会被if拆开。参数level用uint8_t,配合High/Low两个宏,可读性和安全性都够。如果工程是 C89 模式(Keil 默认),inline要写成__inline,这是编译器扩展,不是标准关键字,写在头文件里被多个 .c 包含时会重复定义,稳妥起见放 .c 里、头文件只留声明。
3.3 停车到底用双高还是双低
这是设计上的一个真实选择,不是细节。
双低(IN1=0、IN2=0)是自由停车,H 桥四个管子全关,电机两端悬空,靠惯性慢慢停下来。适合“松开按键让它自然滑行”的场景,比如小车松开前进键继续溜一段。
双高(IN1=1、IN2=1)是刹车,两个下桥臂同时导通,电机绕组被短接,反电动势形成制动电流,停得快、停得干脆。原料选的是这个,对“按下停止键就要立刻停”的演示场景更合适,也更容易在答辩时展示出“按下去就停”的确定感。
代价是刹车瞬间电流较大,电源余量不足时会引起电压跌落,MCU 可能复位。所以我一般会看电源:升压板带得动就用双高,带不动就退回双低,或者先双高 200ms 再切双低,兼顾制动和电流。这个切换逻辑放在Motor_x_STOP()里包一层就行,调用方不用改。
4. 代码落地:motor 与 key 两个模块怎么分工
整个工程只有四个源文件加两个头文件:main.c负责调度,motor.c/.h管方向,key.c/.h管输入,delay.c提供毫秒延时。软件层面不依赖任何第三方插件,标准外设库(StdPeriph)加启动文件就够,这也是这套方案适合当课设模板的原因——换台电脑拷过去就能编。
4.1 电机模块的初始化与六个动作函数
初始化函数只做三件事:开时钟、配引脚、拉低初值。
#include "motor.h" #include "stm32f10x.h" #include "stm32f10x_rcc.h" void Motor_12_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 四个 IN 引脚都在 GPIOA,一次开时钟即可 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 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_50MHz; /* 驱动能力 */ GPIO_Init(GPIOA, &GPIO_InitStructure); /* 上电先给全低,避免初始化瞬间电机乱动 */ GPIO_ResetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5); }三个参数的取舍值得说一句。GPIO_Mode_Out_PP选推挽而不是开漏,是因为 L298N 的输入端内部有下拉/上拉网络,需要 MCU 主动给出确定的高电平,开漏输出高时是高阻,靠外部上拉,边沿会变缓,长导线下容易受干扰。GPIO_Speed_50MHz是输出翻转速率档位,不是“跑多快”,电机控制这种毫秒级切换用 2MHz 都够,设 50MHz 只是图省事,代价是 EMI 稍大。最后那句GPIO_ResetBits很关键:复位后引脚是浮空输入,如果不主动拉低,L298N 输入端处于不确定状态,会出现上电瞬间电机抖一下的现象。
六个动作函数就是真值表的直译:
/* 电机 A:正转 = IN1 低 / IN2 高 */ void Motor_1_PRun(void) { IN1(Low); IN2(High); } /* 电机 A:反转 = IN1 高 / IN2 低 */ void Motor_1_NRun(void) { IN1(High); IN2(Low); } /* 电机 A:停止 = 双高刹车 */ void Motor_1_STOP(void) { IN1(High); IN2(High); } /* 电机 B 同理,换到 IN3 / IN4 */ void Motor_2_PRun(void) { IN3(Low); IN4(High); } void Motor_2_NRun(void) { IN3(High); IN4(Low); } void Motor_2_STOP(void) { IN3(High); IN4(High); }拆成六个函数而不是一个Motor_Set(uint8_t ch, uint8_t dir),看起来啰嗦,但对课设和毕设前期很实用:调用处语义清晰,Motor_1_PRun()一眼知道在干什么,不用回头查枚举值。等要加 PWM 调速了,再重构成带通道参数的形式也不迟,那时候你已经知道哪些地方真的需要参数化。
4.2 按键扫描、消抖与优先级设计
按键部分用的是“上拉输入 + 软件消抖 + 状态标志”的经典结构。三个键里,KEY0、KEY1 接在 GPIOE 上配成上拉输入(按下读到 0),WK_UP 接 PA0 配成下拉输入(按下读到 1)——两种极性相反,是因为开发板上 WK_UP 那颗键硬件上接的是 VCC,其余两颗接的是 GND。配置模式必须跟硬件电路匹配,配反了的表现是“不按也算按下”或者“怎么按都没反应”,这是新手最常见的翻车点之一。
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; /* 无键按下 */ }几个参数的含义:mode为 0 时是“单次触发”,按住不放只返回一次键值,适合“按一下切换一个状态”;为 1 时每次调用都重置key_up,按住会连续返回,适合“按住持续动作”。原料主循环里传的是 0,也就是按一下动一下、松手才能再按,这个对正反转切换更安全——连按模式下快速切正反转,H 桥会频繁换向,电流冲击大。
delay_ms(10)这个消抖时长是经验值,机械按键抖动一般 5~20ms,10ms 覆盖大多数轻触开关。如果按键是那种行程长、手感软的自锁开关,可能要加到 20ms;加太长会让操作变迟钝。另外这个函数里if-else的判断顺序决定了优先级:KEY0 > KEY1 > WK_UP,同时按下时只有 KEY0 生效。原料注释里也写了这一点,做毕设答辩时如果被问到“两个键一起按会怎样”,这就是标准答案。
4.3 主循环调度与上电缓冲
主循环是个很朴素的轮询结构:
int main(void) { vu8 key = 0; Motor_12_Config(); /* 先配电机引脚,保证上电就是停止态 */ delay_init(); delay_ms(10000); /* 上电缓冲,等电源和升压板稳定 */ KEY_Init(); /* 再配按键,避免误触发 */ while (1) { key = KEY_Scan(0); if (key) { switch (key) { case WKUP_PRES: /* 正转 */ Motor_1_PRun(); Motor_2_PRun(); break; case KEY1_PRES: /* 反转 */ Motor_1_NRun(); Motor_2_NRun(); break; case KEY0_PRES: /* 停止 */ Motor_1_STOP(); Motor_2_STOP(); break; } } else { delay_ms(10); /* 无键按下时小睡,降低空转占用 */ } } }初始化顺序是有意安排的:先配电机再配按键。如果反过来,按键先初始化完,主循环前的这 10 秒里如果有人碰了按键,虽然不会执行动作(循环还没进),但心理上容易误判成“按键失灵”。先让电机进入确定的停止态,再进入等待,逻辑上更稳。
delay_ms(10000)这个十秒缓冲是原料里的写法,作用是等升压板和电源稳定——尤其是升压板,从 5V 升到 12V 有个软启动过程,输出没稳之前电机可能转得很弱。实际调试时我会把它缩到 200~500ms,否则每次复位都要等十秒,改一次代码等十秒,一天下来能浪费半小时。等验收演示前再改回去,或者干脆用按键触发启动,都比干等强。
else分支里的 10ms 延时不是消抖(消抖在KEY_Scan里面),而是降低轮询频率。没有这个延时,主循环会以几十万次每秒的速度空转,虽然不影响功能,但功耗和干扰都上去了。10ms 的节奏意味着最快 100Hz 的按键响应,人手的操作完全够用。
5. 避坑与排查:电机不转、按键失灵、一上电就自转的三类现场问题
下面这几条都是我在类似项目里真实遇到或者帮别人定位过的,按“现象—原因—解决”写,照着对号入座能省不少时间。
5.1 电机类:嗡嗡响不转与只能单向转
现象一:上电后电机发出“嗡”的一声,轴微微抖动但不转,用手拨一下能转起来但很没力。
原因基本是功率不足或者电源接错。两种情况最常见:一是用 USB 的 5V 直接接 L298N 的 12V 端(没经过升压),电压不够,电机启动扭矩上不去;二是把外部 5V 也接到了 L298N 的 5V 端子上,跟板载稳压顶牛,逻辑电平被拉低。解决:确认 12V 端子上的电压实测在 9~12V 之间,5V 端子悬空,用万用表量一下电机两端的电压,正转时应该有接近电源电压的压差。
现象二:一个电机正反转都正常,另一个只会往一个方向转。
先看代码,Motor_2_*三个函数是不是都调到了;再看接线,IN3、IN4 是不是有一根虚接或者焊点开裂。还有一种可能是 L298N 那一路 H 桥已经烧了(比如电机堵转太久),判断方法是对调两个电机的输出端——如果故障跟着电机走,是电机的问题;跟着驱动板走,是板子的问题。这类排查用替换法最快。
5.2 按键与引脚类:按了没反应、串键与移植报错
现象三:程序烧进去,三个键按了都没反应,偶尔乱跳。
这个坑很隐蔽,出在初始化代码里:
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_3; /* 想配 KEY0/KEY1 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; /* 又被覆盖成 PE5 */ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOE, &GPIO_InitStructure);第二次赋值把第一次覆盖掉了,实际只初始化了 PE5,而KEY_Scan读的是 PE3、PE4——这两个脚从上电起就是复位默认的浮空输入,读到的值随机,表现出来就是时灵时不灵。解决很简单,合并成一句GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5,或者分成两次GPIO_Init调用。这类“结构体成员被重复赋值”的问题编译器不会报错,只能靠人看,所以初始化代码写完一定要逐行念一遍。
现象四:换成 C8T6 最小系统板后,编译报GPIOE未定义,或者编译过了但按键完全没反应。
原因就是前面提过的型号差异:C8T6 没有 GPIOE 端口。解决是把 KEY0、KEY1 重新分配到 PB 口(比如 PB1、PB11,都是普通 GPIO,配上拉输入),同时改key.h里的读取宏和KEY_Init里的时钟使能——注意 PB 属于 APB2 上的 GPIOB,RCC_APB2Periph_GPIOB要一起打开。WK_UP 在 PA0 上不用动,C8T6 有这个脚。移植时还有一点:C8T6 的 PA0 是 WKUP 引脚,如果工程里开了待机唤醒相关配置,可能有冲突,普通输入用的话一般没事。
5.3 干扰类:电机一转 MCU 就复位
现象五:按键控制都正常,但电机一转起来,MCU 就重启,或者按键开始乱跳。
这是典型的电源和地线问题。电机是感性负载,换向和启停时会产生反向电动势,如果电机电源和逻辑电源共用一条细线,或者两块板之间的地线太细太长,浪涌就会顺着地线窜进 MCU,触发复位。解决分三步:一是功率地和信号地单点连接,用稍粗的线,长度尽量短;二是电机两端并一个 0.1μF 的瓷片电容(有的电机出厂就带了),抑制刷片火花;三是给 MCU 的电源脚就近加 100nF 去耦电容。如果条件允许,逻辑侧单独用一路电源(比如开发板 USB 供电),只共地不共电源,效果最明显。
6. 验收与答辩:用六条测试用例证明这套驱动真的跑通了
功能跑通和“能拿出来验收”是两回事。答辩现场最怕的是老师说“你按一下我看看”,结果一按不动,你满头大汗去拨线。我的习惯是在交之前自己走一遍用例表,每条都打勾,心里才有底。
| 编号 | 操作 | 预期现象 | 判据 |
|---|---|---|---|
| T1 | 上电,不按任何键 | 两个电机静止不动 | 无抖动、无异响 |
| T2 | 按 WK_UP(正转) | 两电机同向转动 | 转向一致,转速平稳 |
| T3 | 按 KEY1(反转) | 两电机反向转动 | 与 T2 方向相反 |
| T4 | 按 KEY0(停止) | 两电机立即停转 | 无明显惯性滑行(刹车生效) |
| T5 | 按住某键不放 | 只动作一次,不连续触发 | 松手后再按才再次响应 |
| T6 | 运行 5 分钟后触摸 L298N 散热片 | 温热但不烫手 | 手感能停留 3 秒以上 |
T5 验证的是mode=0的单次触发逻辑,T6 是很多人忽略的一条:L298N 在 12V、两路电机同时工作的状态下会有明显发热,如果散热片烫到不能碰,说明电流超了或者有堵转,长时间跑会烧板。这一条写进验收表,答辩时主动提,反而显得你考虑过工程边界。
答辩问答一般围着“为什么”转,提前准备好这几个:为什么 IN 引脚用推挽输出而不是开漏(要主动给确定电平);为什么停止用双高(刹车,停得干脆,代价是瞬间电流大);为什么必须共地(电平参考基准);为什么 5V 端子不接(板上是输出,接了会和外部电源冲突);换 C8T6 要注意什么(没有 GPIOE,按键要重映射)。这五个问题答利索,基本就稳了。
最后说个我自己的习惯。早些年我第一次接这套东西,电机不转,查了两小时代码,最后发现是 L298N 的 GND 只接了电源负、没接开发板。从那以后我每次接电机线,都强制走一遍流程:断电、照着接线表逐行打勾、万用表量通断和电压、再上电。慢是慢了点,但比事后当黑匣子猜要快得多。这套东西不复杂,复杂的是人总觉得自己不会接错。希望帮到你。
本文还有配套的精品资源,点击获取