☰
STM32F103C8T6驱动L298N:按键控制电机正反转与避坑
2026/11/2 12:27:19 网站建设 项目流程

简介:这是一份面向嵌入式初学者与单片机课程实践者的实战参考资料,围绕 STM32F103C8T6 主控搭配 L298N 双 H 桥驱动模块控制两台直流电机,聚焦正转、反转与停止三种基本动作的实现问题,属于入门级电机驱动与 GPIO 控制练习。资源包共 1 个文件,为 PDF 格式的图文笔记,约 6.97MB,内容依次覆盖器件清单、硬件接线与代码分析三部分。硬件部分说明 OUT1~OUT4 与两台电机的对应关系、IN1~IN4 分别接入 PA2~PA5 的控制方式、12V 端口与公共 GND 的接法,并特别提示 5V 端口不可接电源、升压板供电的注意事项。代码部分给出 main.c 的按键扫描主循环、motor.c 的 GPIO 推挽输出初始化以及停止、正转、反转六组电平组合函数,并附 motor.h 中宏定义的引脚封装写法,可直接对照移植。已有 939 人学习,适合用来快速理清 L298N 与 STM32 的联调思路、排查电机不转或方向相反等常见现象。

1. 按键按下电机只响不转:这套 STM32F103C8T6 驱动 L298N 的代码到底管哪一段

小车底盘装好、轮子拧紧、STM32F103C8T6 最小系统板插上 USB,L298N 的 IN1~IN4 按教程一根根接到 PA2 到 PA5,代码烧进去,按下按键——电机发出一声很闷的嗡响,轮子纹丝不动。这画面我见过太多次,问题十有八九不在 C 代码里,而在电源和共地上。

这套用 STM32F103C8T6 按键控制 L298N 驱动两路直流电机正反转与停止的代码,做的事情很朴素:三个按键映射到正转、反转、停止,四个 GPIO 输出高低电平,交给 L298N 内部的 H 桥去换向。它没有 PWM 调速、没有编码器闭环,但它是一个能真正跑起来的最小闭环,适合刚拿到最小系统板和 L298N 模块、想先让轮子转起来再谈后面那堆东西的人。跑通它,你后面加调速、加 PID 才有地基。

2. 硬件链路拆解:从 PA2~PA5 到 L298N 的 IN1~IN4 怎么接才不烧板卡

这一章把信号链路和电源链路分开讲。信号链路是 STM32 的四个 GPIO 到 L298N 的四个输入脚,简单;电源链路才是真正让人翻车的地方,尤其是手上没有 12V 电源、只能用一块升压板硬顶的时候。

2.1 器件清单与供电拓扑:5V 端子为什么不能随便接

先把这套东西的家底列清楚,避免拿到手发现少件。

器件说明关键参数
STM32F103C8T6 最小系统板主控,48 脚,64KB Flash / 20KB RAM中容量,启动文件用 md 版
L298N 双 H 桥模块驱动两路直流电机或一路步进电机电机电压 5~35V,每路持续 1A、峰值 2A
直流减速电机 ×2常见 3~6V 或 12V 型号堵转电流往往是额定值的 3~5 倍
升压板把 5V 抬到 12V 给电机用看输出功率,别只看输出电压
独立 12V 电源有条件就上,最省心电流建议 ≥2A
杜邦线若干信号线和地线地线至少两根,别省

L298N 模块上通常有三组端子。一侧是 OUT1、OUT2 和 OUT3、OUT4,分别接两个电机;中间是 12V、GND、5V,12V 是电机电源输入,GND 是公共地,5V 是板载稳压芯片的输出;另一侧是 ENA、IN1、IN2、IN3、IN4、ENB,其中 ENA 和 ENB 上默认插着跳线帽。

这里有个必须讲清楚的细节。模块上那颗 5V 是三端稳压器(常见 7805)从 12V 降压之后引出来的,它一头喂给 L298N 内部的逻辑电路,一头引到端子上对外输出。如果你把外部 5V 从这个端子灌进去,而跳线帽还插着,等于两个电源输出端直接短接,电压会被拉低、纹波变大,逻辑电路工作在边缘状态,表现出来就是电机没劲、只嗡嗡响。原文里那句「5v 端口不接,不然很难转动」,说的就是这件事。

还有一个更隐蔽的问题:L298N 内部是双极型晶体管搭的 H 桥,导通时有饱和压降,典型值在 1.4V 到 2V 之间。也就是说 12V 供电,电机端实际只能拿到 10V 出头;如果直接拿 5V 供电机,电机端只剩 3V 左右,小电机勉强能抖两下,稍微带点负载就彻底不动。这就是为什么原文要用升压板把 5V 抬到 12V——它不是多余,是被压降逼出来的。

提示:用升压板之前先算功率。5V 升 12V、效率按 85% 算,输出 12V/1A 需要输入差不多 5V/2.8A。很多小板子标称 2A 输出,实际带电机启动电流直接掉压保护,表现就是按键一按,板子重启、电机抽一下。留一倍余量不亏。

2.2 接线映射与共地处理:一根 GND 决定通信成败

把引脚对应关系固定下来,后面调代码就不用再翻接线。

STM32F103C8T6L298N作用
PA2IN1电机 A 方向控制位 1
PA3IN2电机 A 方向控制位 2
PA4IN3电机 B 方向控制位 1
PA5IN4电机 B 方向控制位 2
GNDGND(中间端子)逻辑电平参考地,必须接
—OUT1 / OUT2接电机 A 两根线
—OUT3 / OUT4接电机 B 两根线
—ENA / ENB保留跳线帽,全速使能

选 PA2 到 PA5 不是随便挑的。这四个脚在 C8T6 上都是普通 GPIO,推挽输出没问题。但它们有第二功能:PA2/PA3 是 USART2 的 TX/RX,PA4/PA5 是 SPI1 的 NSS/SCK。你如果同时要接串口调试或者 SPI 外设,就得换脚,或者接受复用冲突。我一般会把这四个脚留给电机,串口挪到 PA9/PA10,SPI 挪到 PB3~PB5,省得后面打架。

共地是这一节的重点。L298N 的 IN1~IN4 是单端输入,它判断高低电平是拿自己的 GND 当参考的。STM32 输出 3.3V,如果两块板的地没连在一起,各自的「0V」不在一个电位上,L298N 看到的可能是 3.3V 加一个浮动偏置,运气好能识别成高,运气不好直接判成低,甚至电流从信号线倒灌进 GPIO。原文说「中间的 GND 引出两条线,一接板子上 GND,二接电源负」,就是这个道理——一条给逻辑参考,一条给功率回路。

电机线接反了不用担心,不会烧。OUT1 和 OUT2 接电机 A 的两根线,如果转的方向和你预期相反,把这两根线对调,或者把 OUT3/OUT4 那组对调就行。代码里的「正转」只是把 IN1 拉低、IN2 拉高,它本身没有绝对方向的概念,方向是接线和代码共同定义的。

2.3 上电顺序与首次通电自检

接线全部完成之后再上电,顺序上我习惯先给 STM32 上逻辑电(USB 供电),确认板子上的电源灯正常、程序能跑,再给 L298N 上电机电源。反过来也行,但一定要保证所有地线在通电之前就连好,不要带电插拔地线。

第一次通电别急着按按键,先跑一段自检。把下面这个函数加到 main 里,编译烧录,观察两个电机依次动作。

#include "motor.h" #include "delay.h" /* 上电自检:四路输出依次动作,用来验证接线和供电是否正常 */ void Motor_SelfTest(void) { Motor_1_PRun(); /* 电机 A 一个方向 */ delay_ms(1000); Motor_1_STOP(); /* 刹车停住 */ delay_ms(500); Motor_1_NRun(); /* 电机 A 反方向 */ delay_ms(1000); Motor_1_STOP(); delay_ms(500); Motor_2_PRun(); /* 电机 B 一个方向 */ delay_ms(1000); Motor_2_STOP(); delay_ms(500); Motor_2_NRun(); /* 电机 B 反方向 */ delay_ms(1000); Motor_2_STOP(); }

这段代码的逻辑很直白:每个电机先正转一秒、刹车半秒,再反转一秒、刹车半秒。中间那 500ms 的刹车时间不是摆设,它让转子彻底停下来,你能清楚看到「停—转—停—反转」的节奏,而不是靠惯性糊过去。

参数上唯一能调的是延时长度。1000ms 太短看不清,太长容易让电机堵转发热,一秒左右刚好。如果某个电机完全没反应,先别改代码,把万用表打到直流电压档,量 L298N 对应 OUT 端子对 GND 的电压:正转时应该在电源电压减去 1.5V 左右,反转时符号相反。量出来是 0,问题在使能或者电源;量出来正常但电机不转,问题在电机线或者电机本身。

注意:自检阶段不要长时间让电机堵转。轮子卡住的时候电流会飙到额定值的好几倍,L298N 那小块散热片撑不了多久,手摸上去烫就得停。

3. 工程骨架与 GPIO 初始化:motor.c 里那四行推挽输出到底做了什么

硬件通了,接下来是代码。这套工程是标准外设库的组织方式,文件不多,但每个文件的位置和职责要想清楚,不然移植到自己的模板工程里会漏东西。

3.1 五个文件的分工:哪些要拷,哪些是模板自带的

先看文件清单。

文件职责移植时是否需要
main.c初始化调用、主循环、按键分发需要,把 while 循环内容搬过去
motor.c电机 GPIO 初始化、六个动作函数需要,整份拷贝
motor.h引脚宏定义、函数声明需要,整份拷贝
key.c按键 GPIO 初始化、扫描函数需要,但要改引脚
key.h按键读取宏、键值宏、函数声明需要,要改引脚
delay.c / delay.h毫秒和微秒延时模板工程一般自带
sys.hu8/u16/u32 类型定义、位带宏模板工程一般自带

移植路径很明确:找一个能编译通过的标准外设库空工程,把 motor.c、motor.h、key.c、key.h 四个文件加进工程目录,在 IDE 里添加到 Groups,然后把 main.c 里的初始化调用和 while 循环内容抄过去。原文说「如果有官方库,在里面找一个具有这些配置的工程,把几个文件添加进去就可」,指的就是这条路。

有一点要提醒。key.c 里包含了 sys.h,而 sys.h 在多数标准库模板里会带位带操作宏和类型重定义。如果你用的模板没有这个文件,编译会直接报 u8 未定义。解决办法是在 key.h 里自己补一行typedef unsigned char u8;,或者干脆把 key.c 里的 u8 全换成 uint8_t,加上#include <stdint.h>。我倾向后者,可移植性好一些。

3.2 Motor_12_Config 逐行:时钟、模式、速度、初值

初始化函数不长,但每一行都有讲究。

#include "motor.h" #include "stm32f10x.h" #include "stm32f10x_rcc.h" #include "delay.h" void Motor_12_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 局部结构体,别定义成全局 */ /* GPIOA 挂在 APB2 总线上,必须先开时钟,否则后面写寄存器全是空操作 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* 一次配置四个脚:PA2 / PA3 / PA4 / PA5 */ 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); /* 上电默认全部拉低:IN1=IN2=0,电机处于自由停车状态 */ GPIO_ResetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5); }

第一行开时钟是整个函数里最容易漏、后果最严重的一步。STM32 的外设默认时钟是关闭的,你不开时钟,GPIO_Init 里的配置写不进寄存器,程序编译、烧录、运行都不报错,就是引脚毫无反应。这种问题没有任何错误提示,只能靠经验排查,属于典型的黑匣子故障。

GPIO_Mode_Out_PP是推挽输出。推挽的意思是高电平时上面那个 MOS 管导通、往外出电流,低电平时下面那个导通、往里灌电流,两种状态都有驱动能力。L298N 的 IN 脚输入阻抗很高,需要的电流很小,推挽完全够用。如果配成开漏输出,高电平就变成高阻态,必须外接上拉电阻才能输出高,直接接 L298N 会一直识别成低电平。

GPIO_Speed_50MHz这个参数被误解得最多。它说的是引脚电平翻转的最大速率,影响的是输出波形的上升沿和下降沿陡不陡,跟能带多大负载、能输出多大电流完全没关系。对 L298N 的 IN 脚来说,2MHz 就够了,配 50MHz 只是沿更陡、EMI 稍微大一点,功能上没差别。后面加 PWM 调速的时候这个参数才开始有意义。

最后的 GPIO_ResetBits 把四个脚拉低。按照 L298N 的真值表,IN1=0、IN2=0 对应的是自由停车,电机两端悬空,转子靠惯性滑行。但要注意,motor.c 里的Motor_1_STOP()用的是双高电平,那是刹车模式,两者行为不一样。初始化状态和停止函数不一致,算是个小瑕疵,后面第 5 章会具体说。

3.3 IN1~IN4 宏封装的三个隐患

motor.h 里用宏把四个引脚的操作包了一层,写起来确实短。

#define High 1 #define Low 0 #define IN1(a) if (a) \ GPIO_SetBits(GPIOA, GPIO_Pin_2);\ else \ GPIO_ResetBits(GPIOA, GPIO_Pin_2)

这段宏能跑,但埋了三个雷。

第一个是缺少do { } while (0)包裹。宏展开之后是一串裸语句,如果写成if (x) IN1(High); else ...,展开后 else 会跟宏内部的 if 配对,产生悬垂 else,逻辑直接错乱,而且编译器不一定报警。

第二个是没有加花括号。宏体内只有一个 if-else,看起来没问题,但一旦以后有人在里面加第二行语句,就会出问题。

第三个是参数没有加括号。IN1(a)里的 a 如果传进来一个表达式,比如IN1(x > 0),展开后是if (x > 0),这个还好;但如果传进来IN1(a & 0x01),某些情况下运算优先级会出岔子。

改成下面这样就没问题了。

#define IN1(a) do { \ if (a) GPIO_SetBits(GPIOA, GPIO_Pin_2); \ else GPIO_ResetBits(GPIOA, GPIO_Pin_2); \ } while (0)

更进一步的做法是干脆不用宏,写成静态内联函数或者普通函数。宏在调试时没法打断点,出错只能看反汇编,改起来很痛苦。四个函数每个三行,加起来也不长,可读性比宏好得多。我现在的习惯是:能用函数就不用宏,除非这个操作在中断里被调用、对开销极其敏感。

提示:GPIO_SetBits的第二个参数是位掩码,可以一次传多个引脚。GPIO_SetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_4)会把 PA2 和 PA4 同时拉高,这在你需要两个电机同步动作的时候能省一次函数调用。

4. 控制逻辑与按键分发:H 桥真值表怎么变成正转、反转和刹车

GPIO 配好之后,剩下的事就是把「按键」翻译成「引脚电平组合」。这一层逻辑不复杂,但有几个概念必须先对齐,否则调起来全靠猜。

4.1 H 桥真值表:为什么停止要用双高而不是双低

L298N 每一路电机由两个输入脚控制,四个组合对应四种状态。

IN1IN2H 桥状态电机表现
00上下桥臂全关自由停车,转子靠惯性滑行
01一组桥臂导通一个方向转动
10另一组桥臂导通反方向转动
11下桥臂同时导通刹车,绕组短路,快速停住

对照 motor.c 里的六个函数。

void Motor_1_STOP(void) { IN1(High); IN2(High); } /* 双高 = 刹车 */ void Motor_1_PRun(void) { IN1(Low); IN2(High); } /* 一个方向 */ void Motor_1_NRun(void) { IN1(High); IN2(Low); } /* 另一个方向 */ void Motor_2_STOP(void) { IN3(High); IN4(High); } void Motor_2_PRun(void) { IN3(Low); IN4(High); } void Motor_2_NRun(void) { IN3(High); IN4(Low); }

注意Motor_1_PRun里 IN1 是低、IN2 是高。按真值表这是「一个方向」,但到底是正转还是反转,取决于电机线怎么接。函数名叫 PRun 只是约定,不是物理事实。你如果发现按下正转键轮子往后走,把 OUT1 和 OUT2 那两根线对调,或者把 PRun 和 NRun 里的电平互换,两种办法等效。

停止用双高,是刹车模式。下桥臂同时导通,电机绕组被短路,转了动产生的反电动势形成电流,产生制动力矩,轮子会很快停住,但不会锁死。如果换成双低,就是自由停车,轮子会靠惯性继续滑一段。做小车的时候这两种要分清:需要精准停车用刹车,需要省电滑行用自由停车。原作者选双高,说明他要的是「按一下立刻停」的手感。

初始化函数里用的是 GPIO_ResetBits 全拉低,也就是双低的自由停车状态,和 STOP 函数的双高不一致。上电瞬间电机是滑行状态,按下停止键之后才进入刹车状态。功能上没大问题,但如果你上电时轮子因为惯性在转,会看到它慢慢滑停而不是立刻刹住,别以为是代码有 bug。

4.2 主循环与 switch 分发:三个按键怎么落到六个函数

main.c 的结构是最经典的超级循环。

#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "motor.h" #include "delay.h" #include "key.h" int main(void) { u8 key = 0; Motor_12_Config(); /* 四个 IN 脚配成推挽输出 */ delay_init(); /* systick 延时初始化 */ delay_ms(10000); /* 等外部电源稳定,偏保守 */ KEY_Init(); /* 按键引脚初始化 */ while (1) { key = KEY_Scan(0); /* 0 = 不支持连按 */ if (key) { switch (key) { case WKUP_PRES: /* 正转 */ Motor_1_PRun(); Motor_2_PRun(); delay_ms(1); break; case KEY1_PRES: /* 反转 */ Motor_1_NRun(); Motor_2_NRun(); delay_ms(1); break; case KEY0_PRES: /* 停止 */ Motor_1_STOP(); Motor_2_STOP(); delay_ms(1); break; } } else { delay_ms(10); /* 无按键时让出 CPU,顺便参与去抖 */ } } }

三个按键对应三个动作,正转和反转都是两个电机同时动作,所以各调两次函数。delay_ms(1)在动作之后基本没有实际意义,电机响应时间是毫秒级的,这 1ms 改变不了什么,删掉也不影响。无按键时的delay_ms(10)有点用,它降低了循环频率,减少 CPU 空转,同时也让 KEY_Scan 内部的去抖延时不会叠加得太频繁。

KEY_Scan(0)里的 0 表示不支持连按。按住按键不放,只会触发一次动作,松开再按才会触发第二次。这个行为对电机控制是合适的——你不会希望按住正转键电机就一直转,那样松开时机不好控制。如果想要「按住持续转、松开就停」,得改成KEY_Scan(1),然后在主循环里处理按键释放的逻辑。

u8 key = 0;前面那个vu8在原文里是 volatile 修饰。局部变量加 volatile 其实没意义,它只在被中断服务函数或者多线程共享的时候才需要。这里加上去是照抄模板的习惯,编译器不会因此生成更差的代码,但也起不到任何保护作用。

4.3 KEY_Scan 的状态机:static 变量和 10ms 去抖

按键扫描是这套代码里唯一带点状态机味道的地方。

u8 KEY_Scan(u8 mode) { static u8 key_up = 1; /* 静态变量,跨调用保持 */ if (mode) key_up = 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是这个函数的核心。它记录「上一次扫描时按键是不是松开的」。第一次进入时是 1,检测到按下后置 0,之后哪怕按键一直按着,条件key_up && ...也不成立,不会重复触发。直到三个按键全部回到松开状态,key_up 才恢复成 1。这就是「不支持连按」的实现方式。

mode参数为 1 时,每次进函数都把 key_up 重置成 1,于是按住就会不断触发。做菜单翻页的时候常用这个模式,做电机控制一般用 0。

去抖用的是delay_ms(10),阻塞式。按下瞬间触点会弹跳,电平在高低之间抖几下,10ms 之后稳定了再读一次,就能滤掉抖动。代价是这 10ms 里 CPU 什么都干不了。对现在这个只有按键和电机的系统,完全能接受;但如果你的主循环里还有串口收包、传感器采样,这 10ms 就会丢数据,那时候得改成定时器扫描 + 状态机的方式。

按键的电气配置在 KEY_Init 里,KEY0 和 KEY1 配成上拉输入,按下接 GND 读到 0;WK_UP 配成下拉输入,按下接 3.3V 读到 1。所以判断条件里前两个用== 0,第三个用== 1。这个细节很容易在移植的时候搞反,搞反了就是按键完全没反应。

还有一个隐藏的优先级:三个判断是 if-else if 串联的,如果同时按下 KEY0 和 KEY1,只有 KEY0 会被返回。原文注释里写的「KEY0 > KEY1 > KEY_UP」就是这个意思。一般不会同时按,但调试的时候如果发现某个键「失灵」,先确认是不是另一个键卡住了。

5. 避坑排查:上电不转、方向反了、按键失灵的五种真实原因

这一章按「现象 → 原因 → 解决」来写,都是我在调这类板子时真遇到过的。遇到问题按顺序对照,能省不少时间。

5.1 电机只嗡一声不转,或者转得极慢

现象:按键按下,电机发出低沉的嗡嗡声,转子轻微抖动但不旋转,用手拨一下能转半圈又停。

原因:供电电压被压降吃掉了大半。L298N 内部双极型 H 桥的饱和压降在 1.4V 到 2V 之间,5V 供电时电机端只剩 3V 出头,带减速箱的电机堵转扭矩不够,起不来。另一种可能是升压板功率不足,电机启动瞬间电流冲高,升压板触发过流保护,输出电压瞬间塌陷。

解决:换成独立 12V 电源给 L298N 的 12V 端子,电流能力至少 2A。如果只能用升压板,选输出 3A 以上的型号,并且在 L298N 的 12V 和 GND 之间并一颗 470μF 以上的电解电容,吸收启动瞬间的电流冲击。同时确认 L298N 的 5V 端子没有外接电源、跳线帽保持短接状态。

5.2 转起来了但方向反了,或者两个轮子一正一反

现象:按键按下,电机确实转,但方向和预期相反;或者同一个正转指令下,左边轮子往前、右边轮子往后,小车原地打转。

原因:直流电机的转向由两根线的电流方向决定,接线顺序没有统一标准。代码里的 PRun/NRun 只是电平组合的命名,不代表物理方向。两个电机接反的概率各占一半,同时接反或者只反一个都正常。

解决:单个电机方向反了,把 OUT1 和 OUT2 那两根电机线对调。两个轮子转向相反,把其中一个电机的两根线对调,或者在代码里把那个电机的 PRun 和 NRun 内容互换。后者更干净,不用拆线,但要注意以后换板子的时候别忘了解释。我一般选对调线,因为代码里 PRun 就是正转,改了反而容易误导后面看代码的人。

5.3 按键完全没反应,串口打印键值一直是 0

现象:程序烧进去,电机初始化正常,但按任何键都没反应,加串口打印发现 KEY_Scan 一直返回 0。

原因:key.c 里用的是 GPIOE 的 Pin_3 和 Pin_4。STM32F103C8T6 是 48 脚封装,只有 PA、PB 和部分 PC 端口,根本没有 GPIOE。这段代码是从大容量芯片(比如 ZET6,144 脚)的工程里搬过来的,引脚定义没改。开 GPIOE 时钟那个 RCC_APB2Periph_GPIOE 在 C8T6 上对应的位是保留位,写了也没用,读取 GPIOE 的寄存器会得到未定义值或者直接触发硬件异常。

解决:把按键改到 C8T6 实际存在的引脚上。比如 KEY0 用 PB1、KEY1 用 PB0、WK_UP 用 PA0,然后在 key.h 里把宏改成对应的端口和引脚,KEY_Init 里的时钟使能改成 RCC_APB2Periph_GPIOB 和 RCC_APB2Periph_GPIOA。改完之后用万用表确认按键按下时电平确实翻转,再跑程序。这个问题属于血泪级别,第一次遇到能查一整天。

5.4 一上电 L298N 就发烫,或者 STM32 频繁复位

现象:通电几秒钟 L298N 的散热片就烫手,或者程序跑着跑着突然从头开始执行,串口打印乱码。

原因:两种情况。一是共地没接好,STM32 和 L298N 的 GND 之间没有低阻通路,电机电流回流时走信号线,把 GPIO 灌穿或者拉低电源。二是 5V 端子冲突,外部 5V 和板载稳压输出打架,电压跌落导致 STM32 欠压复位。

解决:先用万用表蜂鸣档量 STM32 的 GND 和 L298N 中间那根 GND 端子,必须导通。然后断开 L298N 的 5V 端子上所有外部连线,只保留跳线帽。如果还是发烫,检查电机是不是堵转、ENA/ENB 跳线帽是不是接触不良。发烫严重的话加一块小散热片,L298N 持续 1A 以上不加散热撑不住。

5.5 编译通过但程序跑飞,或者一进中断就 HardFault

现象:Keil 或 IAR 编译零错误零警告,烧录成功,但程序运行不正常,有时候死在启动文件里,有时候进 HardFault 出不来。

原因:启动文件和芯片容量不匹配。C8T6 是中容量产品,64KB Flash,对应的启动文件是 startup_stm32f10x_md.s,预编译宏要定义 STM32F10X_MD。如果工程是从大容量板子的模板拷来的,用的是 startup_stm32f10x_hd.s 和 STM32F10X_HD,中断向量表长度和实际芯片对不上,中断一触发就跳到错误地址。

解决:在工程选项的 C/C++ 预定义宏里改成 STM32F10X_MD, USE_STDPERIPH_DRIVER,把启动文件换成 md 版本,重新编译。同时检查 system_stm32f10x.c 里的时钟配置,C8T6 外部晶振一般是 8MHz,倍频到 72MHz,如果模板里配的是别的频率,延时函数的时间会整体偏差。

注意:这一条和硬件无关,纯粹是工程配置问题,但表现得很像硬件故障,容易把人带偏。遇到玄学问题先确认启动文件和宏定义,能排除掉一大半。

6. 二次开发改造:从按键点动到 PWM 调速的最小改动路径

这套代码最值得改的地方是调速。现在只有全速正转、全速反转和刹车三档,实际做小车根本不够用。改动路径其实很短:把 L298N 上 ENA 和 ENB 的跳线帽拔掉,让这两个脚悬空,然后把它们接到 STM32 有定时器通道的引脚上。

PA6 和 PA7 是 TIM3 的 CH1 和 CH2,正好一对。接线改成 PA6 接 ENA、PA7 接 ENB,IN1~IN4 保持原样。代码上加一段 PWM 初始化。

/* TIM3_CH1(PA6) / TIM3_CH2(PA7) 输出 PWM,频率 = 72M / (psc+1) / (arr+1) */ void Motor_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); /* TIM3 挂 APB1 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; /* 复用推挽,不能写 Out_PP */ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler = psc; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC2Init(TIM3, &TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE); } /* duty 取 0~100,直接映射成比较值 */ void Motor_SetSpeed(u8 ch, u8 duty) { u16 ccr = (u16)(((u32)duty * (TIM3->ARR + 1)) / 100); if (ch == 1) TIM_SetCompare1(TIM3, ccr); else TIM_SetCompare2(TIM3, ccr); }

参数上,arr和psc决定频率。取 psc=71、arr=999,得到 72MHz / 72 / 1000 = 1kHz,这个频率下电机听不到啸叫,调速也够线性。频率太低电机会抖,太高了 L298N 的开关损耗上来,发热明显。GPIO_Mode_AF_PP这一行写错成Out_PP是最常见的翻车点,写错了 PWM 出不来,引脚就是普通 GPIO 电平。

再往下改就是把六个散函数收进一个结构体,方向、速度、目标速度放一起,主循环里跑一个简单的状态机。这样后面加编码器、加 PID 的时候,接口不用推倒重来。

我现在的习惯是:任何一套从按键开始的控制代码,第一次跑通之后都强制走一遍「加一路 PWM、测一次最低启动占空比」。因为电机的死区电压因型号而异,有的 20% 才转,有的 10% 就动了,这个数不測出来,后面写闭环全是空中楼阁。希望帮到你。

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

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

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

立即咨询