资讯动态

STM32F103C8T6+L298N双电机正反转控制与避坑

发布时间:2026/11/6 5:46:32 来源:尧图企业网站定制
简介面向STM32入门与电机控制实践者的实操资料围绕STM32F103C8T6通过L298N双H桥驱动两路直流电机实现正转、反转与停止展开适合正在做课程设计、毕业设计或智能小车底盘的读者参考。压缩包共1个文件为PDF文档约6.97MB内容涵盖器件清单、L298N与STM32的接线关系、电源与共地注意事项以及main.c、motor.c、motor.h、key.c等关键代码的解析能帮助读者对照完成GPIO推挽输出配置与按键扫描控制。已有939人学习可作为快速搭建电机驱动实验、排查电机不转或转向相反等问题的参考并附电源管理、中断响应与编码器测速等优化思路便于在此基础上扩展。1. 从“电机不转”说起这套 STM32F103C8T6 L298N 方案到底能干什么第一次把 L298N 接上 STM32F103C8T6电机不转很多人第一反应是回去翻代码看 GPIO 是不是写反了。真正常见的翻车点却是电源和共地L298N 的 GND 没和单片机 GND 接在一起信号就是飘的。这套资源解决的就是这个最小闭环——用 PA2~PA5 四根线驱动 L298N 的两个 H 桥按键控制两个直流电机正转、反转、停止代码基于 STM32 标准库motor.c、motor.h、key.c、main.c 分工清楚。适合刚学完 GPIO 输出、想找一个能动手的电机驱动 Demo 的人也适合拿它当底板后面加 PWM 调速、加编码器。它不追求精致但把“按键→GPIO→H 桥→电机”这条链路完整摆出来了。2. 硬件链路拆解PA2~PA5 四根线怎么把 L298N 的双 H 桥管起来这套东西的硬件不复杂但顺序错了会怀疑人生。先把 L298N 当什么、四根 IN 线管什么讲清再谈电源。很多人把 L298N 当成“电机电源板”其实它是逻辑控制加 H 桥功率级逻辑电平和电机电压是两回事。单片机只负责告诉 L298N 往哪个方向导通真正推电机的是 12V 那一路。2.1 L298N 内部两个 H 桥与 IN1~IN4 的真值表L298N 是双 H 桥能同时驱动两个直流电机或一个步进电机。模块上 OUT1/OUT2 是一组OUT3/OUT4 是另一组。IN1/IN2 控制第一组的导通方向IN3/IN4 控制第二组。ENA/ENB 是两个桥的使能模块上一般用跳帽短接默认使能拔掉跳帽后可以用 PWM 调速。ENAIN1IN2OUT1-OUT2电机状态101低-高一个方向110高-低另一个方向100低-低刹车/停止111高-高刹车/停止0xx高阻自由滑行表格里“一个方向”不写正转反转因为正反转是相对的。代码里Motor_1_PRun是 IN10、IN21它只是约定。实际电机往哪边转取决于 OUT1/OUT2 到电机两根线的接法。方向反了最省事的做法是对调电机线不是去改代码里的 High/Low否则两个电机同时动作时很容易一个正一个反。再说停止。原文Motor_1_STOP是 IN1High、IN2High两个输出同时高电机两端被短接属于刹车停止不是自由滑行。刹车停止对小车来说更稳但电流会大一点如果要让电机自由滑行得把 ENA 拉低。这个区别在后面做 PWM 调速时会碰到调占空比时如果一直用双高停止电机其实是“刹”住的不是自然减速。2.2 电源与共地12V、5V、GND 到底怎么接L298N 模块上那排端子通常标着 12V、GND、5V。12V 是电机电源输入模块上叫 Vs不是非得 12V7~12V 常见电压越高电机越有劲但别超过模块和电机的额定。5V 是模块板载稳压器的输出当 12V 端有电时5V 端可以给单片机或其他逻辑电路供电。它不是给电机供电的输入。原文说“5V 端口不接不然很难转动”本质是把 5V 当输入用了电机电压只有 5V当然没劲。GND 是命门。L298N 的 GND 必须和 STM32 的 GND 接在一起否则 IN1~IN4 的高电平没有共同参考L298N 可能乱动作或者不动作。原文专门引出两条 GND一条接板子 GND一条接电源负这个做法是对的。升压板的输入负、输出负、STM32 的 GND、L298N 的 GND最后要共到一个点。L298N 端子接到哪里说明12V升压板输出正 / 独立电源正电机电源别接 5VGND电源负 STM32 GND必须共地5V不接或只给单片机供电板载稳压输出不要灌 5VIN1PA2电机1方向IN2PA3电机1方向IN3PA4电机2方向IN4PA5电机2方向OUT1/OUT2电机1两根线方向反了就对调OUT3/OUT4电机2两根线方向反了就对调ENA/ENB跳帽短接默认使能调速时接 PWM提示上电顺序建议先给控制板上电再给电机电源上电断电反过来。热插拔电机线容易让 L298N 的 H 桥受冲击。2.3 接线顺序与电机方向反了怎么救接线顺序我一般按“地—逻辑—电机—电源”来先把所有 GND 连好再把 IN1~IN4 接到 PA2~PA5再把电机接到 OUT 组最后接电机电源。这样每一步都能单独测不会一上电就两处错。方向反了怎么救两种办法对调同一组 OUT 的两根线或者对调 IN1/IN2 在代码里的 High/Low。我更推荐对调电机线。原因是代码里Motor_1_PRun和Motor_2_PRun的语义是统一的如果只改一个电机的代码后面两个电机同时动作时就会出现一个正一个反排查起来很烦。对调线是硬件层修正代码保持干净。还有一个细节先单独测电机1确认正反转和停止都正常再接电机2。两个电机一起上电源功率不够时电压会被拉低表现是“转一下就停”或者“只嗡不转”很容易误判成代码问题。这一步多花五分钟后面能省半小时。3. 工程结构落地motor.c / motor.h / key.c / main.c 各管什么这套代码不大但文件职责分得清楚。main.c 只做初始化和按键分发motor.c 管 GPIO 和电机动作motor.h 放宏和函数声明key.c/key.h 管按键。把它塞进一个标准库工程基本就能跑。下面按目录、初始化、宏三个层面拆重点讲“为什么这么写”和“改哪里会翻车”。3.1 目录与文件职责从标准库工程模板往里塞文件标准库工程一般长这样CMSIS 放内核和启动文件StdPeriph_Driver 放外设库User 放 main.c 和用户文件Startup 放startup_stm32f10x_md.s。C8T6 是中容量启动文件选 md 后缀。把 motor.c、motor.h、key.c、key.h、delay.c、delay.h 放到 User 或 HARDWARE 目录然后在 IDE 里 Add Existing Files 加进工程Include Paths 里加上对应目录。Project/ ├── CMSIS/ ├── StdPeriph_Driver/ ├── Startup/ │ └── startup_stm32f10x_md.s └── User/ ├── main.c ├── motor.c ├── motor.h ├── key.c ├── key.h ├── delay.c └── delay.hkey.h 里用了 sys.h这是某套常见标准库模板里的文件提供位带操作和类型定义。如果你手里的模板没有 sys.h不要硬加直接把 KEY0/KEY1/WK_UP 三个宏改成GPIO_ReadInputDataBit就行。main.c 里 include 了 motor.h、key.h、delay.h编译报“找不到头文件”时先查 Include Paths不要先怀疑代码。注意标题写的是 C8T6正文里出现过 zet6 的字样。PA2~PA5、PA0、PE3/PE4 在 C8T6 上都有按最小系统板的丝印接就行如果板子引脚排布不同以原理图为准。3.2 Motor_12_Config 的 GPIO 初始化逐行拆void Motor_12_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 定义GPIO初始化结构体 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开GPIOA时钟 GPIO_InitStructure.GPIO_Pin GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5; // IN1~IN4 GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 50MHz GPIO_Init(GPIOA, GPIO_InitStructure); // 写进GPIOA GPIO_ResetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5); // 上电先拉低 }逐行说RCC_APB2PeriphClockCmd开的是 APB2 上 GPIOA 的时钟标准库不自动开时钟忘了这句后面写寄存器全无效。GPIO_Pin_2到GPIO_Pin_5对应 PA2~PA5正好是 L298N 的 IN1~IN4。GPIO_Mode_Out_PP是通用推挽输出驱动 L298N 的逻辑输入够用不要配成开漏除非外面有上拉。GPIO_Speed_50MHz是输出翻转速度电机方向控制不需要这么快但标准库模板里常这么写不影响。最后GPIO_ResetBits把四个脚拉低避免上电瞬间 IN1/IN2 同时为高进入刹车或者两个电机乱动一下。参数上如果要把电机换到别的引脚改GPIO_Pin_x和GPIO_Init的端口同时改 motor.h 里 IN1~IN4 宏对应的端口和引脚。只改一处另一处不改编译能过但电机不动这是最常见的“代码没报错但不干活”。3.3 motor.h 里的宏为什么用 if/else 包 GPIO_SetBits#define High 1 #define Low 0 #define IN1(a) if (a) \ GPIO_SetBits(GPIOA, GPIO_Pin_2); \ else \ GPIO_ResetBits(GPIOA, GPIO_Pin_2) #define IN2(a) if (a) \ GPIO_SetBits(GPIOA, GPIO_Pin_3); \ else \ GPIO_ResetBits(GPIOA, GPIO_Pin_3) /* IN3、IN4 同理对应 PA4、PA5 */这个宏把“高/低”变成可读的IN1(High)、IN1(Low)。但它是多语句宏没有do{}while(0)包起来。单独写IN1(High);没问题如果写成if (flag) IN1(High); else ...展开后 else 会跟宏里的 if 配对逻辑就乱了。这是 C 语言宏的经典坑血泪经验是多语句宏一律用do{}while(0)或者干脆写成static inline函数。改进写法static inline void IN1_Set(uint8_t level) { if (level) GPIO_SetBits(GPIOA, GPIO_Pin_2); else GPIO_ResetBits(GPIOA, GPIO_Pin_2); }static inline有类型检查调试时也能单步进去比宏好维护。原来的宏保留也行但别在带 if/else 的复杂表达式里用。电机控制函数本身很直void Motor_1_PRun(void) { IN1(Low); IN2(High); } void Motor_1_NRun(void) { IN1(High); IN2(Low); } void Motor_1_STOP(void) { IN1(High); IN2(High); }这里Motor_1_PRun只负责把 IN1/IN2 置成 0/1不关心电机实际朝向。两个电机同时调用才是“两个轮子一起正转”。如果你只改其中一个函数另一个没改就会出现原地打转。4. 按键扫描与主循环KEY_Scan 的状态机与电机动作映射硬件和电机驱动就位后剩下的就是“人怎么指挥”。这套代码用三个按键KEY0 停止、KEY1 反转、WK_UP 正转。按键扫描不是简单读电平里面有上拉/下拉、消抖、连按、优先级四个点。主循环把键值映射到电机函数逻辑很直但细节没配好就会“按了没反应”或者“手一碰就乱跳”。4.1 KEY_Init 的上拉/下拉输入配置void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOE, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_3; // KEY0/KEY1 GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOE, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; // WK_UP GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; // 下拉输入 GPIO_Init(GPIOA, GPIO_InitStructure); }PE3/PE4 配上拉输入按键另一端接 GND松开时高电平按下时低电平所以KEY00表示按下。PA0 配下拉输入WK_UP 另一端接 3.3V松开时低电平按下时高电平所以WK_UP1表示按下。两种极性相反读代码时容易看反。原文里有一行GPIO_InitStructure.GPIO_Pin GPIO_Pin_5;紧跟着又被GPIO_Pin_0覆盖属于多余赋值不影响运行但读起来会愣一下。自己整理代码时删掉就行。按键引脚模式按下电平宏判断KEY0PE4上拉输入低KEY00KEY1PE3上拉输入低KEY10WK_UPPA0下拉输入高WK_UP14.2 KEY_Scan 的消抖、连按与优先级u8 KEY_Scan(u8 mode) { static u8 key_up 1; // 按键松开标志 if (mode) key_up 1; // mode1 支持连按 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 key_up是核心按下一次只返回一次松手后 key_up 复位才能再触发。mode0时不支持连按按住只算一次mode1时每次调用都把 key_up 置 1等于支持连按。main.c 里传的是 0所以是单次触发。优先级是 KEY0 KEY1 WK_UP因为 if-else 链先判断 KEY0。三个键同时按只认 KEY0。这个优先级在电机控制里不算大问题但要知道。delay_ms(10)是阻塞消抖这 10ms 里 CPU 干不了别的。对按键控制电机来说够用如果后面要加串口、定时器采样最好改成定时器计数消抖别在扫描里死等。另外消抖时间别加到 50ms 以上连按会明显发木按一下要等半天才有下一次。4.3 main.c 主循环三个按键对应正转/反转/停止int main(void) { vu8 key 0; Motor_12_Config(); // 电机GPIO初始化 delay_init(); // 延时函数初始化 delay_ms(10000); // 上电等10秒 KEY_Init(); // 按键初始化 while (1) { key KEY_Scan(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); } } }初始化顺序有讲究先配电机 GPIO再初始化延时再按键。delay_ms(10000)是等 10 秒原文说等电源稳定但调试时这 10 秒很折磨建议改成 100~500ms等升压板输出稳定就行。按键初始化放在延时后面问题不大但如果你想上电就能按键把KEY_Init提前到延时之前。switch 里每个分支后面的delay_ms(1)基本没用可以删。两个电机同时调用正转/反转/停止中间没有加减速直接切换电流冲击比较大电源余量不够时正反转切换瞬间容易复位或掉压。后面做 PWM 软启动能缓解。还有一点这套主循环是“查询式”的按键不按就delay_ms(10)CPU 一直在空转。做demo没问题做产品要改成中断或定时器任务否则加不了别的功能。5. 避坑与排查五个真实翻车现场的现象、原因和解决这套东西代码量不大坑基本都在硬件和时序上。下面五条是我按“现象—原因—解决”整理的顺序按出现频率排。别一上来就改代码先量电压、量地、量电平。5.1 电机只嗡不转 / 转一下就停现象上电后电机发出嗡嗡声轴不转或者转半圈就停松手又转一下。原因最常见是电源功率不够。L298N 模块标称 2A但升压板从 5V 升到 12V 时输入电流会被拉大5V 电源本身限流一启动就掉压。其次是共地没接IN 信号没有参考。还有 5V 端子本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑