
简介这份STM32F103C8T6控制L298N驱动直流电机的学习资料面向嵌入式入门者与需要完成电机正反转实验的读者重点解决双直流电机启停、正反转控制及L298N与单片机接线问题。压缩包内为1个PDF文件约6.97MB集中呈现硬件连接说明、GPIO初始化、按键扫描、电机正反转与停止控制代码以及电源供电和调试注意事项。内容按部件、接线、代码与优化建议展开读者可据此理解IN1IN4电平组合与电机动作的对应关系并参考主程序、电机驱动和按键处理代码搭建自己的控制框架。资料中还提到5V供电、升压板使用、共地连接和方向调整等实践细节便于排查电机不转或转向异常。已有939人学习下载适合作为课程实验、毕业设计前期验证和STM32外设练习的参考。1. 一块板子两个轮子L298N 这套驱动代码能跑出什么效果很多人第一次做双轮小车最后卡住的地方不是代码写不出来而是接线那十分钟。这套基于 STM32F103C8T6 的例程做的事情其实非常克制四个 GPIO 推挽输出接到 L298N 的 IN1~IN4两个按键负责发指令一个按键让两个直流电机正转一个按键让它们反转第三个按键让它们停。没有编码器没有 PWM没有串口协议就是最纯粹的方向控制闭环里最小的那一环。它能让你在半小时内看到轮子转起来、换向、刹停从而确认「主控活着、驱动活着、电源扛得住」这三件事。适合刚上手 STM32 标准库、想拿电机练手又不想一上来就被 FOC 和 PID 劝退的人也适合手上已经有一块 C8T6 最小系统板和一块 L298N 模块想找一份能直接编译下载的参考代码的人。下文所有引脚、逻辑电平、代码结构都来自这份工程我按自己复现时踩过的顺序重排了一遍。2. 硬件链路拆解从 PA2~PA5 到 IN1~IN4 的接线与共地在代码之前先把线接对否则后面所有调试都是在给错误的物理连接写注释。L298N 是一颗双 H 桥逻辑侧要的是电平信号功率侧要的是电机电压这两套电源必须在 GND 上汇合否则 STM32 输出的 3.3V 高电平对 L298N 来说没有参考点芯片会「听不懂」。这一章把接线拆成三块信号线、功率线、以及验证方法。2.1 引脚映射表与接线顺序先看原作者给的映射关系我用一张表固定下来接线时对着这张表一根根插不要凭记忆。L298N 端子STM32F103C8T6 引脚作用备注IN1PA2电机 A 方向位 1推挽输出3.3V 电平IN2PA3电机 A 方向位 2推挽输出IN3PA4电机 B 方向位 1推挽输出IN4PA5电机 B 方向位 2推挽输出OUT1 / OUT2电机 A 两根线功率输出方向反了就对调这两根OUT3 / OUT4电机 B 两根线功率输出同上ENA板载跳线帽短接电机 A 使能调速时才需要接 PWMENB板载跳线帽短接电机 B 使能同上12V外部电源正极电机功率输入常见 7~12VGND外部电源负极 STM32 的 GND公共地必须接且要接牢5V悬空板载稳压输出见 2.2 的说明接线顺序建议是先断所有电接 GND再接 IN1~IN4再接电机线最后接电源。反过来接的话插 IN 线时手指很容易蹭到 12V 端子和 GND 之间的间距运气不好就是一股青烟。电机线不分正负接反了只是转向相反不会烧这一点可以放心。2.2 电源与共地最容易翻车的一环原始记录里那句「5v 端口不接不然很难转动」是整份材料里最值钱的一句话值得单独解释。L298N 模块上那个标着 5V 的端子在跳线帽通常标为 5V-EN 或者 U5V短接时是板载 7805 稳压器的输出用来给单片机供电拔掉跳线帽后它就变成外部 5V 输入给 L298N 自己的内部逻辑供电。很多人把它当成「给模块供电」的入口直接拿 5V 电源怼上去结果就是 L298N 内部逻辑和电机共用一路被拉垮的 5V电机一启动电压瞬间跌到 3V 出头表现出来的现象就是轮子只抖不转、或者转得极慢同时单片机还可能复位。我在复现时的做法是把跳线帽保留5V 端子完全不接电机功率走 12V 和 GNDSTM32 用另一路 5VUSB 或者最小系统板自己的稳压供电两路电源的负极在 L298N 的 GND 端子上汇合。这一根地线是所有问题的分水岭——不接L298N 收不到有效电平接虚了电机一转程序就跑飞。原文提到手上只有 5V 电源、用了升压板升到 12V这是可行的应急方案但要注意升压板的输出电流能力。L298N 驱动两个空载小电机静态电流也有几十毫安堵转时单路能到 1A 以上一块常见的廉价升压模块往往标称 2A 实际连续输出也就 1A 出头。空载测试没问题一装上轮子落地就重启八成是这里。条件允许的话直接用 12V 电池盒或者 12V 适配器比升压方案稳得多。2.3 用万用表验证接线的三步法接完线别急着上电烧程序花两分钟做三步验证能省掉后面半小时的怀疑人生。第一步断电状态下用万用表蜂鸣档量 STM32 的 GND 和 L298N 的 GND应该导通。量 5V 端子和 GND不应该导通如果导通说明跳线帽拔了但外部 5V 又接上了短路。第二步只给 STM32 上电L298N 的 12V 先不接用万用表直流电压档量 PA2~PA5 对 GND 的电压程序里初始化把这些引脚拉低正常应该都是 0V 左右。这一步同时验证了主控在工作、引脚没被复用功能占走。第三步给 L298N 上 12V量 5V 端子对 GND如果是 5V 左右说明板载稳压正常、跳线帽在位如果测出来是 0V 或者接近 12V说明跳线帽状态不对先别接电机。提示L298N 内部是双极型晶体管 H 桥导通压降典型值在 1.4V~2V 之间12V 进去电机实际只能拿到 10V 出头这是芯片的物理特性不是接线问题。想要更高效率就得换 MOS 方案的驱动模块但这套代码的逻辑层不用改。3. 核心代码拆解motor.h 四个宏把电平翻译成转向这套工程的组织方式是很典型的标准库模板main.c 管流程motor.c/motor.h 管执行器key.c/key.h 管输入delay 提供毫秒延时。分开看每个文件都不复杂但合在一起有几个细节值得说清楚——尤其是 motor.h 里那四个宏它是整份代码里唯一一处「把硬件操作包装成语义」的地方。3.1 工程文件划分移植时最省事的做法是找一个能编译通过的空模板工程然后把新增文件塞进去。目录结构大概是这样Project/ ├── CORE/ # 内核文件模板自带不用动 ├── STM32F10X_FWLib/ # 标准外设库源码 ├── SYSTEM/ │ ├── delay/ # 毫秒/微秒延时 │ └── sys/ # 位带操作等基础宏 ├── HARDWARE/ │ ├── motor/ # motor.c motor.h本次核心 │ └── key/ # key.c key.h ├── USER/ │ ├── main.c │ ├── stm32f10x_conf.h # 头文件开关 │ └── stm32f10x_it.c # 中断入口 └── OBJ/ # 编译输出需要新增到工程里的只有 motor.c 和 key.c 两个源文件其余都是模板原有内容。这一步的坑在启动文件和器件宏上留到第 4 章讲。3.2 motor.h宏定义把寄存器操作藏起来先看头文件它定义了电平别名和四个引脚操作宏#ifndef __MOTOR_H #define __MOTOR_H #include stm32f10x.h #include stm32f10x_gpio.h #define High 1 #define Low 0 /* 每个宏内部用 do{}while(0) 包一层避免在 if/else 里展开时出错 */ #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这里的逻辑说明宏IN1(a)接收参数 aa 为真非 0时把这个引脚置高为假时置低。参数a用的是第 2 章表里定义好的High/Low也就是 1 和 0。之所以用宏而不是函数是因为这四次调用在电机动作函数里是热路径宏展开可以省掉一次函数调用的压栈开销——虽然在 72MHz 的 F1 上这点开销根本感知不到但它带来的写法好处是代码读起来像「设置 IN1 为高」比直接写GPIO_SetBits(GPIOA, GPIO_Pin_2)直观。参数说明GPIOA是端口基址GPIO_Pin_2是引脚掩码两者组合定位到具体引脚的 BSRR/BRR 寄存器。GPIO_SetBits写 BSRR 的低 16 位GPIO_ResetBits写 BSRR 的高 16 位都是单周期原子操作不会出现读-改-写的中间态这一点比GPIO_WriteBit更适合电机控制这种对时序有要求的场景。原文的宏没有用do{}while(0)包裹是直接if(a) ... \ else ...。单独调用没问题但如果哪天写成if(cond) IN1(High); else ...展开后 else 会跟宏内部那个 if 配对编译器不报错行为却完全错了。这是个老生常谈的坑我在自己的版本里补上了——这类改动不影响功能纯粹是给以后改代码的自己留条后路。3.3 motor.c初始化与六个动作函数初始化函数只做三件事开时钟、配引脚、拉低。#include motor.h #include stm32f10x_rcc.h void Motor_12_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* PA2~PA5 挂在 APB2 上必须单独开时钟否则写寄存器无效 */ 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 的逻辑输入必须用这个如果误配成开漏GPIO_Mode_Out_OD高电平就是高阻态靠外部上拉拉高L298N 输入端如果没有上拉电阻就会一直读到低电平表现出来的现象是「怎么按都只往一个方向转」。GPIO_Speed_50MHz是输出翻转速率对电机方向控制来说 2MHz 完全够用50MHz 带来的额外边沿陡度在长排线上反而容易串扰到相邻引脚——如果接线比较长降到GPIO_Speed_2MHz更稳。然后是六个动作函数两两成对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); }这里的逻辑说明要对照 L298N 的真值表看单个 H 桥的两路输入组合一共四种状态IN1IN2电机 A 状态电气含义00自由滑行停止两个下桥臂导通电机两端被短接到地01正转或反转取决于电机线序左上 右下导通10反转与上行相反右上 左下导通11刹车停止两个上桥臂导通电机两端被短接到电源注意Motor_x_STOP()用的是IN1(High); IN2(High);也就是真值表最后一行——这是「刹车」而不是「滑行」。刹车状态下电机两端被短接反电动势形成回路会产生一个阻碍转动的制动力矩轮子会很快停下如果改成IN1(Low); IN2(Low);就是滑行轮子靠惯性自由转动慢慢停。做小车的话刹车手感更干脆但频繁刹车对 H 桥的冲击电流更大滑行更省电、更温和。这个选择没有对错取决于你想让小车的停車动作看起来是什么样。原作者选了刹车我复现时保留了因为两个小轮子惯性不大刹车那一下的顿挫感反而更像「听话」。3.4 main.c主循环里的按键状态机主函数的结构很直白初始化、等一会、扫按键、分派动作。#include stm32f10x.h #include motor.h #include delay.h #include key.h int main(void) { vu8 key 0; Motor_12_Config(); /* 先把电机引脚配成输出并拉低 */ delay_init(); /* 延时函数初始化基于 SysTick */ delay_ms(10000); /* 上电等待见下方说明 */ KEY_Init(); /* 按键 GPIO 初始化 */ while(1) { key KEY_Scan(0); /* 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; default: break; } } else { delay_ms(10); /* 没有按键时空转降低 CPU 占用 */ } } }逻辑说明KEY_Scan(0)的参数 0 表示不支持连续按也就是按住不放只会返回一次键值必须松开再按才会触发第二次。这个模式适合「按一下切换一个状态」的场景正是这里需要的——按一下正转电机就一直转直到你按停止。如果传 1按住正转键会每个扫描周期都返回一次键值虽然结果一样都是调Motor_1_PRun但白耗 CPU。参数说明vu8是volatile unsigned charvolatile告诉编译器这个变量可能在中断或硬件中被改不要优化掉。这里其实没有中断参与加 volatile 属于习惯性写法无害。那句delay_ms(10000)我建议直接改掉。原意大概是上电后留出十秒缓冲方便观察或者避开电源不稳的瞬间但实际体验是烧录完复位板子毫无反应十秒钟第一次上手的人百分百会怀疑程序没跑起来。我一般改成delay_ms(500)够电源稳定了。如果确实需要长延时比如等某个外设上电也应该是放在外设初始化里而不是堵在主函数开头。另外switch里原本每个 case 后面还有一个delay_ms(1)这个延时没有任何作用——动作函数是即时生效的延时只会让按键响应慢 1ms。删掉。3.5 key.c软件消抖与优先级按键部分用的是最经典的「检测 延时 再检测」消抖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); /* 机械抖动一般 5~20ms取 10ms */ 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; }逻辑说明key_up是静态变量第一次调用时初始化为 1之后每次调用都保留上次的值。按下瞬间检测到电平变化先延时 10ms 躲过抖动再确认一次然后把key_up清零——这样在松手之前即使电平还在跳也不会重复返回键值。松手判断在else if里三个键全部回到松开态才把标志置回 1。参数说明mode为 0 时是单次触发为 1 时每次进来先把key_up置 1等于跳过「松手才允许下次」的限制变成按住就连发。KEY0/KEY1是上拉输入按下读到 0WK_UP是下拉输入按下读到 1。这两个极性是反的所以判断条件里一个是 0一个是 1抄代码的时候这里最容易看漏。注意这个函数里有一个隐藏的优先级KEY0 KEY1 WK_UP。三个键同时按下时只有 KEY0 会被返回。日常用不会同时按但如果后面要加组合键功能这个顺序就得重新设计。还有一点delay_ms(10)是在扫描函数内部阻塞执行的意味着按键按下时会占用 10ms 的 CPU。这套代码主循环里没别的事无所谓一旦加了串口接收或者 PWM 输出就得换成状态机 定时器扫描的非阻塞写法第 6 章会给改造方案。4. 工程搭建与烧录从空工程到电机转起来代码看懂了接下来是让它真的跑在自己的板子上。这一步的坑不在 C 语言而在工程配置——尤其是芯片型号、启动文件、宏定义这三者必须对齐错一个就是一堆看不懂的链接错误或者运行异常。4.1 时钟、启动文件、器件型号选择STM32F103C8T6 属于中容量Medium Density产品线Flash 64KBRAM 20KB48 脚只有 GPIOA/GPIOB/GPIOC其中 PC13~PC15 可用。工程里必须体现这三点第一Keil 的 Device 选型要选STM32F103C8不要选成STM32F103ZE或者STM32F103RC。选错了编译能过但下载时可能报「Cannot access target」或者下载进去跑飞。第二启动文件要用startup_stm32f10x_md.smd medium density。如果模板是从大容量工程比如 ZET6 的例程拷来的启动文件是startup_stm32f10x_hd.s链接时中断向量表长度对不上表现是中断进不去、或者一上电就 HardFault。第三C/C 选项卡里的全局宏定义要有STM32F10X_MD, USE_STDPERIPH_DRIVER。第一个决定标准库按哪个容量编译外设寄存器映射第二个决定是否启用stm32f10x_conf.h里的外设头文件包含。少写STM32F10X_MD编译会直接报一堆寄存器未定义。这三条我列成清单每次新建工程对着勾一遍[ ] Device : STM32F103C8 [ ] Startup : startup_stm32f10x_md.s [ ] Define : STM32F10X_MD, USE_STDPERIPH_DRIVER [ ] Include Paths : CORE, STM32F10X_FWLib/inc, USER, SYSTEM/delay, HARDWARE/motor, HARDWARE/key [ ] Output : 勾选 Create HEX File用串口 ISP 下载时需要 [ ] Debug : ST-Link / J-Link / DAP 按实际选择Port 选 SW附带说一句这套 stm32 软件是纯标准库工程不需要装任何额外的插件或者包管理器Keil 装好器件支持包Device Family Pack就能编译对网络环境没有要求离线也能干活。4.2 编译与下载把 motor.c、key.c 加进工程分组把对应的头文件路径加进 Include Paths然后按 F7 编译。正常情况下应该是 0 Error 0 Warning。如果出现undefined symbol Motor_12_Config说明 motor.c 没加进工程或者路径不对如果出现cannot open source input file stm32f10x_gpio.h说明标准库的 inc 目录没加进 Include Paths。编译通过后下载方式看手上有什么工具# 用 ST-Link 命令行工具烧录需先装 ST-LINK Utility 或 CubeProgrammer STM32_Programmer_CLI -c portSWD -w OBJ/Project.hex -v -rst # 如果用 DAP-Link OpenOCD openocd -f interface/cmsis-dap.cfg -f target/stm32f1x.cfg \ -c program OBJ/Project.hex verify reset exit参数说明-w指定要写的文件-v表示写入后校验-rst表示写完自动复位运行。用 OpenOCD 的话interface/cmsis-dap.cfg是调试器配置target/stm32f1x.cfg是芯片配置两者都不能省。如果手上只有串口那就用 HEX 文件 串口 ISP 工具注意 BOOT0 跳线要先拨到 1从系统存储器启动下载完再拨回 0否则程序不运行。这个拨码开关忘记拨回来是新手最常见的「下载成功但不跑」的原因。4.3 上电顺序与首次验证第一次通电我建议按这个顺序每一步都观察现象出问题能立刻定位到是哪一步第一步只插 USB 给 STM32 供电不接 L298N 的 12V。此时程序应该已经跑起来了如果你把delay_ms(10000)改小了用万用表量 PA2~PA5默认都应该是低电平。第二步按一下正转键量 PA2 应该变低本来就是低、PA3 变高3.3V。再按反转键PA2 变高、PA3 变低。这一步确认了按键扫描和 GPIO 输出都是通的跟电机没关系。第三步给 L298N 接上 12V把电机接上。再按正转轮子应该转。如果此时单片机复位表现为程序重新从头跑、或者按键失灵几秒立刻断电回到 2.2 节检查电源。第四步测试停止。按下停止键轮子应该迅速停住而不是缓慢滑行——如果是缓慢滑行说明Motor_x_STOP()里的电平写错了回去对着真值表看。注意测试时不要把轮子悬空太久。空载转速高、电流小很多电源问题在空载下看不出来落地带载才会暴露。真要验证电源够不够就用手轻捏轮子施加阻力看单片机是否复位。5. 避坑与排查按键没反应、电机只嗡嗡响、一按就复位这一章是我复现这套代码时真实踩过的五条每条按「现象 → 原因 → 解决」写对着查能省掉大量时间。5.1 现象编译下载都正常但按键怎么按都没反应原因这份代码的按键接在 GPIOE 上KEY0是 PE4KEY1是 PE3而 STM32F103C8T6 是 48 脚封装物理上根本没有 GPIOE。工程能编译通过是因为标准库的头文件对所有 F103 型号都定义了 GPIOE 的寄存器宏编译器不会报错但代码执行到RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE)时这个时钟位在 C8T6 上不存在写进去没有效果后面读 PE4 的输入数据寄存器读回来永远是 0于是KEY0 0恒成立KEY_Scan会一直返回KEY0_PRES停止表现就是「按什么键都只是停」。解决把按键改到 C8T6 实际存在的引脚上。我一般这么改KEY0 用 PB0KEY1 用 PB1都配上拉输入、按下为低WK_UP 保留 PA0配下拉输入、按下为高。key.h 里的宏跟着改/* 适配 C8T6去掉 GPIOE改用 PB0/PB1 PA0 */ #define KEY0 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) #define KEY1 GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) #define WK_UP GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0)key.c 里的时钟使能也要从RCC_APB2Periph_GPIOE改成RCC_APB2Periph_GPIOB。如果手头的板子上 PB0/PB1 没有焊按键就得外接两个轻触开关一端接引脚、一端接 GND靠内部上拉。5.2 现象电机不转只发出「嗡嗡」声或者轻微抖动原因L298N 的 5V 端子和跳线帽状态错了。跳线帽短接时 5V 是输出端此时如果再往这个端子上灌 5V两路电源打架L298N 内部逻辑电压被拉低H 桥驱动能力不足电机只能抖。另一种情况是跳线帽被拔掉了但没接外部 5VL298N 逻辑部分没供电同样不转。解决检查跳线帽。用外部 12V 供电机、STM32 自己独立供电的场景下跳线帽保持短接5V 端子悬空不接。用万用表量 5V 对 GND正常应该是稳定的 5V 左右如果测出来只有 2~3V 或者随电机抖动那就是被拉垮了立刻断开 5V 端子。5.3 现象一按正转键单片机就复位或者程序跑飞原因电机启动瞬间的浪涌电流把共用电源拉低到单片机的复位阈值约 2.0V以下。根源通常是两路电源共地不良、或者升压板/电池的输出能力不够。解决分三步。第一确保 STM32 的 GND 和 L298N 的 GND 用一根短而粗的线连接不要用杜邦线串一长串。第二电机电源和单片机电源分开走只共地不共正。第三在 L298N 的 12V 和 GND 之间并一个 100μF 以上的电解电容吸收启动瞬间的电流尖峰。这三步做完还有复位那就是电源本身功率不够换 12V 电池盒。5.4 现象上电后要等十秒才响应按键以为程序死了原因main 函数开头那句delay_ms(10000)。解决改成delay_ms(500)或者直接删掉。如果确实需要等待某个外设稳定用状态机 定时器实现不要堵在主循环前面。5.5 现象两个电机转向不一致或者跟预期相反原因电机线的接法没有统一。直流电机两根线不分正负同一个方向的电流接反了就是反转。另外两个电机如果是从不同批次买的内部磁钢方向也可能是反的。解决先分别单独测试每个电机。给Motor_1_PRun()看电机 A 转向再给Motor_2_PRun()看电机 B 转向。如果 B 和 A 反向把 B 的两根线OUT3/OUT4对调。不要靠改代码里的电平来纠正那样以后每换一个电机都得重新改代码。硬件的归硬件软件的归软件。6. 二次开发把点动正反转改成 PWM 调速和非阻塞状态机这套代码能用但它的定位是「验证链路通不通」离能用还有两段距离一是速度不可调二是按键扫描是阻塞的。这两点在二次开发里几乎一定会被要求改掉下面是我实际改过的两个方向。6.1 加 PWM 调速把 ENA/ENB 从跳线帽接到定时器L298N 的 ENA/ENB 是使能端跳线帽短接时是常使能全速。要调速必须先拔掉跳线帽把这两根针接到能输出 PWM 的引脚上。C8T6 上 PA6/PA7 正好是 TIM3 的 CH1/CH2而且跟已经占用的 PA2~PA5 不冲突是现成的选择。/* TIM3_CH1(PA6) - ENA, TIM3_CH2(PA7) - ENB, 72MHz / 72 1MHz 计数时钟, 1000 个计数 1kHz PWM */ 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); /* PA6/PA7 必须配成复用推挽普通推挽输出不了 PWM */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_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_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; /* 初始占空比 0 */ TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); /* CH1 - PA6 */ TIM_OC2Init(TIM3, TIM_OCInitStructure); /* CH2 - PA7 */ TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }参数说明arr是自动重装值psc是预分频。调用时传Motor_PWM_Init(999, 71)计数时钟 72MHz / (711) 1MHzPWM 周期 1000 个计数 1ms也就是 1kHz。这个频率对 L298N 来说比较合适——太低低于 100Hz电机会有可闻的啸叫和抖动太高高于 20kHzL298N 的开关损耗会明显上升、芯片发热。1kHz~5kHz 是比较稳的区间。有了 PWM 之后把原来的动作函数改成带速度参数的形式速度用有符号数表示正负代表方向/* speed: -1000 ~ 1000正为前进负为后退0 为停止 */ void Motor_SetSpeed(s8 id, s16 speed) { if(speed 0) { if(id 1) { IN1(Low); IN2(High); TIM_SetCompare1(TIM3, speed); } else { IN3(Low); IN4(High); TIM_SetCompare2(TIM3, speed); } } else if(speed 0) { if(id 1) { IN1(High); IN2(Low); TIM_SetCompare1(TIM3, -speed); } else { IN3(High); IN4(Low); TIM_SetCompare2(TIM3, -speed); } } else { if(id 1) { IN1(High); IN2(High); TIM_SetCompare1(TIM3, 0); } else { IN3(High); IN4(High); TIM_SetCompare2(TIM3, 0); } } }6.2 按键改成非阻塞扫描把延时从主循环里赶出去原来的KEY_Scan里有一个delay_ms(10)这 10ms 里 CPU 什么都干不了。加了 PWM 之后其实影响不大PWM 是硬件外设不占 CPU但如果再加串口接收、OLED 刷新这个阻塞就会变成肉眼可见的卡顿。改法是定时器每 5ms 中断一次在中断里采样按键电平用一个状态机做消抖主循环只读结果。验证改造是否成功的办法很朴素按键按下的同时用示波器量 PWM 输出看占空比切换是否干净、有没有丢帧没有示波器就接个串口每 100ms 打印一次当前速度和按键状态看串口输出是否连续、有没有掉数据。改造这种东西能观测就能调试。最后说个我自己的习惯。每次改完电机相关的代码我不会直接上整车测试而是先在桌上把两个轮子悬空依次按正转、反转、停止、再正转看四个动作是否都符合预期再看电流表读数有没有异常尖峰。因为电机这东西翻车成本太高——一次接线错误可能烧掉的不只是 L298N还有那块主控。从那以后我每次动电机线之前都强制走一遍「断电 → 量通断 → 再上电」的流程慢是慢了点但没再烧过板子。希望帮到你。本文还有配套的精品资源点击获取