☰
STM32F103C8T6与L298N按键控制直流电机正反转实战
2026/11/2 3:16:28 网站建设 项目流程

简介:面向 STM32 入门与嵌入式课程实验的按键控制直流电机实践文档,主控为 STM32F103C8T6,驱动采用 L298N 双 H 桥,讲解如何通过板上按键实现两台直流电机的正转、反转与停止。硬件部分说明 OUT1~OUT4 与两台电机的对应接法、IN1~IN4 分别接 PA2~PA5 的控制信号、12V 供电与 GND 共地处理,以及仅用 5V 电源时借助升压板、5V 端口悬空的注意事项。代码部分给出 main.c、motor.c、motor.h 与 key.c 的完整实现,涵盖 GPIO 推挽输出初始化、引脚电平宏定义与按键扫描分支,可直接移植到自己的工程。文件共 1 个,为 PDF 类型,压缩包约 6.97MB,已有 939 人学习下载;文末另附电源管理、中断优化、编码器测速与错误处理等改进方向,适合需要快速跑通电机驱动实验的初学者。

1. 电机只哼不转时先看这套代码:F103C8T6 与 L298N 的最小闭环

电机只哼一声不转,按键按下去没反应,L298N 的指示灯亮着但轮子不动——这类现场问题往往不是代码全错,而是电源、共地、引脚和驱动真值表有一处没对齐。这套 STM32F103C8T6 控制 L298N 驱动直流电机的代码,结构上更像一个能直接塞进标准库工程的软件插件:main.c 做调度,motor.c/motor.h 管两路电机的正反转与停止,key.c/key.h 管按键扫描。它适合手里有 F103C8T6 最小系统板、L298N 模块和两个直流电机,想把“按键控制电机正反转”这条链路一次跑通的人。原料里有一处写成了 ZET6,接线和代码按标题与摘要的 C8T6 落地;换芯片只改时钟和引脚宏,不要把型号混着抄。

2. 硬件接线先立住:L298N 与 F103C8T6 的 7 根线怎么落

硬件连接决定代码有没有执行环境。F103C8T6 的 PA2-PA5 输出 3.3V 逻辑电平,L298N 的 IN1-IN4 接收方向指令,OUT1-OUT4 把电流送到两个直流电机。真正容易翻车的不是信号线,而是 12V、5V、GND 这三个电源端子的边界:12V 是电机电源,5V 端子多数模块上是内部稳压输出或逻辑电源,GND 必须和单片机共地。先把这些端子认清,再谈代码里的正反转。

2.1 电源与共地:12V、5V、GND 三个端子的边界

L298N 模块上通常有一排电源端子:12V、GND、5V。12V 端子接电机电源正极,范围常见是 5V 到 35V,但模块标称和实际散热要留余量;GND 接电源负极,同时必须引一根线到 STM32 的 GND;5V 端子不要拿外部 5V 硬怼上去。原料里写“5V 端口不接,不然很难转动”,这句话不是玄学,而是因为很多模块的 5V 端子是板载稳压输出,外部再灌 5V 会和内部稳压打架,逻辑电平被拉偏,L298N 的输入阈值判断就会出问题。正确做法是:12V 正常供电,5V 端子悬空,模块内部自己产生逻辑电源;如果模块没有内部稳压,按模块说明单独给逻辑电源,仍然要和单片机共地。

共地是第二个硬边界。STM32 的 PA2 输出高电平,是相对于 STM32 的 GND 而言的;L298N 判断 IN1 是高还是低,是相对于 L298N 的 GND 而言的。两边 GND 不连,IN1 的高低电平就没有共同参考,现象是电机随机转、抖一下、或者完全不认按键。我一般会先用万用表蜂鸣档量三处:STM32 的 GND 到 L298N 的 GND、L298N 的 GND 到电机电源负极、升压板输出负极到 L298N 的 GND,三处都通才算共地成立。

电源功率也要提前算。L298N 内部是双 H 桥,常见模块标称每桥 2A,实际连续跑 1A 左右就要摸温度;直流电机启动电流往往是额定电流的几倍,升压板从 5V 升到 12V 时输入电流会更大,限流一触发,电压就塌。现象是电机刚想转,单片机复位或者 L298N 指示灯变暗。常见做法是在 12V 输入端并一个 100uF 以上电解电容和 0.1uF 瓷片电容,电机线用粗一点的线,信号线再用杜邦线;升压板的输入电源要能供得上,不要用电脑 USB 口直接带两个电机。

注意:L298N 的 GND、STM32 的 GND、电机电源负极必须回到同一个参考点,只连一根信号线不连地,电机不转是正常结果。

接线表可以按下面这一版落:

L298N 端子接到说明
12V升压板输出正电机电源,不是 5V 逻辑电源
GNDSTM32 GND + 电源负极必须共地
5V不接模块内部稳压输出,外接易冲突
IN1PA2电机 1 方向
IN2PA3电机 1 方向
IN3PA4电机 2 方向
IN4PA5电机 2 方向
OUT1/OUT2电机 1 两根线方向反了交换这两根
OUT3/OUT4电机 2 两根线方向反了交换这两根
ENA/ENB跳线帽短接全速;做 PWM 时拔掉接 PA6/PA7

2.2 控制信号与电机输出:IN1-IN4、OUT1-OUT4 的对应关系

IN1-IN4 是方向控制输入,不是电机电源。F103C8T6 的 PA2、PA3、PA4、PA5 分别接 IN1、IN2、IN3、IN4,代码里用 GPIOA 的 Pin_2 到 Pin_5 推挽输出。L298N 的输入级是 TTL 电平,3.3V 高电平通常能识别为高,前提是共地良好、5V 逻辑电源正常。不要把 IN1-IN4 接到 5V 上直接拉高,也不要把输入脚悬空;悬空时 L298N 输入状态不确定,电机可能自己转。

OUT1/OUT2 是一路 H 桥输出,接电机 1 的两根线;OUT3/OUT4 是另一路,接电机 2。电机方向由 IN1/IN2 的电平组合决定,但最终轮子往哪边转还取决于 OUT1/OUT2 是否反接。如果代码写正转、轮子却反转,不用改代码,把电机 1 的 OUT1/OUT2 对调,或者把电机两根线对调即可;电机 2 同理对调 OUT3/OUT4。这样代码里的正反转定义保持不变,硬件方向靠接线修正,后面排查不会乱。

ENA/ENB 是使能端。很多 L298N 模块默认用跳线帽把 ENA/ENB 短接到 5V,电机全速运行;如果拔掉跳线帽,ENA/ENB 必须接高电平或者 PWM,不能悬空。原料里没有展开 ENA/ENB,因为全速正反转只需要方向控制;但你要做调速,就必须把跳线帽拔掉,把 ENA/ENB 接到定时器 PWM 引脚。这里先记住:全速实验时跳线帽短接,调速实验时拔掉,两种状态不要混。

接线完成后先不要插 STM32 的 USB,先断电用万用表量 12V 和 GND 之间有没有短路,量 5V 端子有没有和 12V 短在一起。确认无误再上电。上电顺序常见做法是:先上 STM32 逻辑电,再上 L298N 的电机电源;断电时反过来,先断电机电源,再断逻辑电。这样避免电机电源波动反灌到单片机。

2.3 上电顺序与最小验证:不写代码也能判断驱动板是否活着

在跑 main.c 之前,先做一次不依赖按键的最小验证:把 L298N 的 ENA 跳线帽短接,IN1 用杜邦线接 3.3V,IN2 接 GND,OUT1/OUT2 接电机 1,12V 和 GND 接好,共地接好。上电后电机 1 应该朝一个方向转;把 IN1 和 IN2 对调,电机应该反向转。这个动作能直接判断 L298N 的输出桥、电源、电机线是否正常。如果这一步不转,后面代码写得再对也没用。

也可以用 STM32 输出一个慢速翻转电平来验证 PA2 到 L298N 的链路。下面这段代码只测 IN1,IN2 先接地,电机 1 会间歇转停;它能帮你确认 GPIO 初始化、共地和 L298N 输入是否通。

#include "stm32f10x.h" #include "delay.h" int main(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_2; // 只测 IN1 gpio.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出,能主动拉高拉低 gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); GPIO_ResetBits(GPIOA, GPIO_Pin_2); // 先给低,避免上电乱转 delay_init(); while(1) { GPIO_SetBits(GPIOA, GPIO_Pin_2); // IN1=1,IN2 接地时为一种方向 delay_ms(1000); GPIO_ResetBits(GPIOA, GPIO_Pin_2); // IN1=0,电机停 delay_ms(1000); } }

这段代码的逻辑很直:PA2 配置为推挽输出,先拉低,再在循环里每 1 秒翻转一次。参数上,GPIO_Speed_50MHz是输出翻转速度,不是电机功率;delay_ms(1000)让现象肉眼可见,不要改成 1ms,否则电机只会抖。IN2 必须接地或由代码拉低,不能悬空;如果 IN2 悬空,电机可能不按预期转。测试时不要长时间堵转,手摸 L298N 散热片明显烫就断电,先查电源和电机线。

3. 把电机动作写成软件插件:motor.c 与 motor.h 的推挽输出和真值表

motor.c/motor.h 是这套资源里最值得直接复用的部分:它把两路电机的正转、反转、停止封装成函数,main.c 只负责按键调度。移植到别的标准库工程时,只要把 GPIO 引脚宏改掉,初始化函数和动作函数可以原样带走。这里有两个细节要拆开:为什么用推挽输出,以及停止到底用双高还是双低。前者决定 L298N 能不能识别高电平,后者决定电机是刹车还是滑行。

3.1 为什么选推挽输出而不是开漏:L298N 输入级的电平需求

STM32 的 GPIO 输出模式常见有推挽和开漏。推挽输出内部有上下管,能主动把引脚拉到高电平和低电平;开漏输出只能主动拉低,高电平要靠外部上拉电阻。L298N 的 IN1-IN4 是高阻抗输入,理论上开漏加上拉也能用,但多一个上拉电阻就多一个接触不良点,调试时容易变成黑匣子。推挽输出直接给 3.3V 高和 0V 低,接线最少,现象最直接。

F103C8T6 的 PA2-PA5 配置为GPIO_Mode_Out_PP,输出高电平约 3.3V。L298N 模块的输入高电平阈值通常在 2.3V 左右,3.3V 够用;如果模块逻辑电源被拉低到 3V 以下,阈值可能漂移,电机就会时转时不转。所以第 2 章反复强调 5V 端子不要乱接、共地要牢。推挽输出还有一个好处:上电初始化时可以把四个引脚先拉低,电机不会因为引脚默认状态乱动。这个初始电平就是后悔药,少了它,上电瞬间电机可能猛转一下。

GPIO_Speed_50MHz经常被误解成“输出功率”。它实际是引脚翻转速度档位,影响边沿斜率,不决定电机电流。电机电流由 L298N 和电源决定,GPIO 只出信号。把 Speed 设成 50MHz 没有坏处,但不要以为改了它电机就更有劲。真正要关注的是 PA2-PA5 有没有被别的外设占用,比如串口、SPI、定时器;F103C8T6 的引脚复用不多,PA2/PA3 还是 USART2 的 TX/RX,如果你同时开串口2,要确认没有冲突。

3.2 motor.h 的宏封装与 motor.c 的初始化:引脚、时钟、初始电平

原始 motor.h 用宏封装 IN1-IN4,写法是if (a) GPIO_SetBits(...) else GPIO_ResetBits(...)。这种写法单独调用没问题,但宏里没有大括号,放进if/else里容易出现悬挂 else 的翻车。下面这版用do{...}while(0)包起来,宏展开后是一个完整语句,复制到别的工程更稳。

#ifndef __MOTOR_H #define __MOTOR_H #include "stm32f10x.h" #define HIGH 1 #define LOW 0 #define IN1(a) do{ if(a) GPIO_SetBits(GPIOA, GPIO_Pin_2); else GPIO_ResetBits(GPIOA, GPIO_Pin_2); }while(0) #define IN2(a) do{ if(a) GPIO_SetBits(GPIOA, GPIO_Pin_3); else GPIO_ResetBits(GPIOA, GPIO_Pin_3); }while(0) #define IN3(a) do{ if(a) GPIO_SetBits(GPIOA, GPIO_Pin_4); else GPIO_ResetBits(GPIOA, GPIO_Pin_4); }while(0) #define IN4(a) do{ if(a) GPIO_SetBits(GPIOA, GPIO_Pin_5); else GPIO_ResetBits(GPIOA, GPIO_Pin_5); }while(0) void Motor_12_Config(void); void Motor_1_STOP(void); void Motor_1_PRun(void); void Motor_1_NRun(void); void Motor_2_STOP(void); void Motor_2_PRun(void); void Motor_2_NRun(void); #endif

motor.c 的初始化只做三件事:开 GPIOA 时钟、把 PA2-PA5 配成推挽输出、把四个引脚先拉低。顺序不要反,先开时钟再配寄存器,否则配置不生效。初始电平拉低对应 IN1-IN4 全低,电机处于自由停转状态,上电不会突然刹车或猛转。

#include "motor.h" #include "stm32f10x_rcc.h" void Motor_12_Config(void) { GPIO_InitTypeDef gpio; 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); } void Motor_1_STOP(void) { IN1(HIGH); IN2(HIGH); } // 双高:刹车停止 void Motor_1_PRun(void) { IN1(LOW); IN2(HIGH); } // 电机1正转 void Motor_1_NRun(void) { IN1(HIGH); IN2(LOW); } // 电机1反转 void Motor_2_STOP(void) { IN3(HIGH); IN4(HIGH); } // 双高:刹车停止 void Motor_2_PRun(void) { IN3(LOW); IN4(HIGH); } // 电机2正转 void Motor_2_NRun(void) { IN3(HIGH); IN4(LOW); } // 电机2反转

这段代码的参数很集中:GPIO_Pin_2到GPIO_Pin_5对应 IN1-IN4,GPIO_Mode_Out_PP决定输出能力,GPIO_ResetBits决定上电初始状态。停止函数用的是双高,L298N 两个输入同时为高时,H 桥下管或上管形成短接制动,电机快速停住;如果电源功率小,刹车瞬间电流大,可能把电压拉塌,现象是按键一停,单片机复位。遇到这种情况,可以把停止改成双低,让电机自由滑行,冲击小,但停得慢。选双高还是双低,不是对错问题,是电源和机械惯量的取舍。

3.3 正转、反转、停止三个动作的真值表与函数实现

把动作写成真值表,排查时不用猜。下面以电机 1 为例,电机 2 把 IN1/IN2 换成 IN3/IN4 即可。表里的“正转”是逻辑定义,实际轮子方向由 OUT1/OUT2 接线决定;如果表里正转、轮子反转,交换 OUT1/OUT2,不要改表。

动作IN1IN2H 桥状态电机表现
正转01一端低一端高朝定义方向转
反转10一端高一端低反方向转
刹车停止11输出端短接制动快速停,电流冲击大
自由停转00输出端高阻惯性滑行,停得慢

主程序里调用Motor_1_PRun()和Motor_2_PRun()就是让两个电机同时正转;调用Motor_1_STOP()和Motor_2_STOP()就是同时刹车。两个电机可以独立控制,也可以像资源里的 main.c 一样成对调用。如果你想让两个轮子差速转向,就分别给电机 1 和电机 2 不同的动作,但注意不要同时给一个电机正转和反转,否则 H 桥上下管直通,模块会发烫甚至保护。

代码移植到别的工程时,最容易漏的是RCC_APB2PeriphClockCmd。只复制 motor.c 不复制时钟使能,GPIO 配置写进去也不起作用,现象是按键有反应但电机不动。另一个漏点是头文件包含顺序:motor.h 里用了GPIO_SetBits,必须包含stm32f10x.h;如果工程里还用了stm32f10x_gpio.h,确认标准库版本一致,不要混用不同版本的外设库。

4. 按键扫描不是配角:KEY_Scan 的去抖、优先级与 C8T6 引脚修正

按键是这套代码的人机入口,但原始 key.c 里藏着两个坑:一是GPIO_InitStructure.GPIO_Pin先赋值GPIO_Pin_4|GPIO_Pin_3,下一行又被GPIO_Pin_5覆盖,实际只初始化了一个引脚;二是 KEY0/KEY1 用了 GPIOE,而 F103C8T6 的 48 脚封装通常没有 GPIOE。标题写 C8T6、正文里却出现 ZET6,问题就出在这里。按键不响应不一定是 KEY_Scan 写错,先查引脚在芯片上存不存在、初始化有没有被覆盖。

4.1 按键硬件:KEY0/KEY1 上拉输入与 WK_UP 下拉输入

常见按键接法有两种:按键一端接 GND,另一端接 GPIO,GPIO 配成上拉输入,按下读 0;按键一端接 3.3V,另一端接 GPIO,GPIO 配成下拉输入,按下读 1。资源里的 KEY0/KEY1 是低有效,WK_UP 是高有效。KEY0/KEY1 配置为GPIO_Mode_IPU,WK_UP 配置为GPIO_Mode_IPD。这样未按下时 KEY0/KEY1 被上拉到高,WK_UP 被下拉到低;按下时电平翻转。

C8T6 的可用引脚要重新映射。原始代码用 PE3/PE4 读 KEY0/KEY1,但 C8T6 没有 PE 口,直接编译可能报错,或者工程里虽然定义 GPIOE 但芯片实际没有对应引脚,读回来是固定值。下面按 PB5/PB6 做修正示例:KEY0 接 PB5,KEY1 接 PB6,按键另一端接 GND,低有效;WK_UP 仍然接 PA0,高有效。如果你的板子按键已经焊在别的引脚,只改宏和初始化里的 GPIOB/GPIO_Pin 即可,逻辑不变。

功能原始引脚C8T6 示例修正有效电平输入模式
KEY0PE4PB5低上拉
KEY1PE3PB6低上拉
WK_UPPA0PA0高下拉

key.h 里把读引脚宏改掉,同时保留键值定义。键值不要和 0 冲突,0 表示无按键。

#ifndef __KEY_H #define __KEY_H #include "stm32f10x.h" #define KEY0 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5) // 低有效 #define KEY1 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6) // 低有效 #define WK_UP GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) // 高有效 #define KEY0_PRES 1 #define KEY1_PRES 2 #define WKUP_PRES 3 void KEY_Init(void); u8 KEY_Scan(u8 mode); #endif

key.c 的初始化要分两次GPIO_Init:先配 GPIOB 的 PB5/PB6 为上拉输入,再配 GPIOA 的 PA0 为下拉输入。不要在一个结构体里先写 PB5/PB6 又写 PA0,因为 GPIOB 和 GPIOA 不是同一个端口,必须分别初始化。原始代码里GPIO_Pin被覆盖的问题,就是结构体复用后没有重新赋值完整导致的。

#include "key.h" #include "delay.h" void KEY_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6; // KEY0/KEY1 gpio.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入,按下为低 gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio); gpio.GPIO_Pin = GPIO_Pin_0; // WK_UP gp <p> <a href="https://download.csdn.net/download/weixin_59919674/89293327" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询