STM32F103C8T6+L298N双电机驱动:正反转、按键与PWM调速
简介围绕STM32F103C8T6与L298N展开的直流电机控制学习材料面向刚接触STM32嵌入式开发、需要完成双电机正反转与停止实验的读者重点解决硬件接线、GPIO输出配置、按键扫描与电机驱动逻辑配合等问题。压缩包内仅含1个PDF文档体积约6.97MB以图文说明和代码片段为主便于在电脑端阅读对照不依赖额外工程文件。文档从L298N的OUT1OUT4与两台电机连接、IN1IN4对应PA2PA5的控制信号入手说明共地、12V供电及5V升压注意事项并给出主循环按键判断、电机初始化及正转、反转、停止函数的实现思路。已有939人学习下载适合用作课堂实验、课程设计或自学练手时的接线与代码参考也可据此排查电机不转、方向相反等常见问题。内容还提示独立供电、中断响应、编码器反馈和错误处理等优化方向方便读者在跑通基础功能后继续扩展。1. 从一块只有 5V 的板子说起L298N 双电机驱动能跑成什么样手上只有一块 STM32F103C8T6 最小系统板和一片 L298N 模块想把两个直流轮子盘活这套组合基本是最低成本的入口STM32F103C8T6 控制 L298N 驱动直流电机四个 GPIO 出方向电平三个按键切换正转、反转、停止主循环里连一个定时器都不用开。配套的main.c、motor.c、motor.h、key.c抠进任意一个标准外设库工程就能编译不用先去啃驱动芯片手册。搜 stm32 软件/插件 的时候你会刷到一堆带 PID、带编码器、带上位机的方案但真正能让人在一小时内看到轮子转起来的往往就是这种四个 IO 一把梭的裸驱动。它适合两类人刚摸完 GPIO 推挽输出、想找个能看见物理反馈的练手项目的新手以及手上有个旧小车底盘、只想先把电机盘活再谈调速的老手。这篇不吹它能做多复杂的运动控制只讲清楚接线怎么落地、代码骨架为什么这么写、以及那些编译通过但轮子不听话的地方到底卡在哪。2. 接线先立住L298N 四个 IN 脚、共地和一个不能乱插的 5V 端子电机驱动这活儿代码写错顶多不转线接错是真的会冒烟。所以在打开编辑器之前先把 L298N 这块板子的引脚分工捋清楚尤其是电源那一段——原文作者踩过的坑八成读者还会再踩一遍。2.1 为什么选 L298N双 H 桥、方向电平和一路使能L298N 内部是两组完整的 H 桥每组四个功率管搭成桥臂通过互补导通改变电机两端的电压极性这就是正反转的物理来源。它不需要你去算 PWM 相位也不需要外部自举电容方向完全由两个逻辑输入决定属于给电平就动的器件。对于 12V、1A 上下的小直流减速电机一块模块带两路绰绰有余。它的控制接口分三层IN1/IN2管第一路方向IN3/IN4管第二路方向ENA/ENB管这一路的使能或者调速。模块出厂时 ENA、ENB 上面通常插着短接帽等于把使能脚硬拉到高电平两路永远是全速。这也是为什么这套代码里一个 PWM 都没写电机照样转——使能是硬件短接的软件只负责方向。有两个参数值得提前记住。第一是输入高电平阈值L298N 的逻辑输入按 TTL 兼容设计高电平门限在 2.3V 左右所以 STM32 的 3.3V 推挽输出可以直接驱动不需要电平转换很多人卡在3.3V 能不能驱动 5V 逻辑这个疑问上答案是能。第二是输出压降L298N 用的是达林顿结构导通时上下管加起来会吃掉 1.4V 到 3V 不等这个压降在小电压供电时是致命的后面第 5 章会专门算这笔账。还有一点容易被忽略L298N 模块背面那块金属片不是装饰。每路持续 2A、峰值 3A 是芯片手册的乐观值实际模块上的敷铜和散热面积撑不住1A 以上跑几分钟就烫手。长期跑建议贴个小散热片或者把两路电机的占空比错开使用。2.2 从 OUT1/OUT2 到 PA2–PA5一张能照着插的接线账电机侧和逻辑侧要分开看。输出侧OUT1、OUT2接第一个电机的两根线OUT3、OUT4接第二个电机接反了不用改代码把这两根线对调就行这是最省事的纠错方式。输入侧IN1到IN4分别落到 STM32 的PA2、PA3、PA4、PA5四个脚在代码里被配置成推挽输出。L298N 端子接到哪里说明OUT1 / OUT2电机 A 两根线方向反了就对调这两根OUT3 / OUT4电机 B 两根线同上IN1 / IN2PA2 / PA3电机 A 方向推挽输出IN3 / IN4PA4 / PA5电机 B 方向推挽输出ENA / ENB默认短接帽在位拔掉后可接 PWM 调速12VVs升压板输出正电机功率电源与逻辑电源分开GND中间STM32 的 GND必须连否则电平没有参考5V悬空不接板载稳压的输出端不是输入端这张表里最需要解释的是最后一行。模块上的5V端子是板载 78M05 稳压器的输出当 Vs 供电正常且板上跳线帽在位时它会对外吐出 5V可以拿来给单片机供电。但它不是让你往里灌 5V 的入口。原文作者写5v 端口不接不然很难转动现象是对的但原因不是接了就不转这么玄学——把外部 5V 硬接到这个端子上等于让板载稳压器的输出和你的电源对着干同时如果 Vs 那一路本身电压就低H 桥内部的达林顿管压降一吃电机两端剩不下几伏自然转不动。稳妥做法就是5V悬空单片机自己走 USB 或者独立 3.3V电机走独立的 Vs。选PA2到PA5这四个脚是有代价的得提前知道。PA2、PA3同时是 USART2 的 TX 和 RXPA4、PA5是 SPI1 的 NSS 和 SCK。也就是说这四路一旦被电机占住串口调试打印和 SPI 屏幕就得另找地方。我一般会把电机挪到PB6到PB9或者PB12到PB15把PA2/PA3留给串口——调电机的时候没有串口打印排错体验会差一个量级。当然如果只是照抄这套代码先跑通用 PA 口也无所谓后面再改就是改四行宏定义。2.3 电源链升压板、共地和那个十秒延时原文作者的处境很真实手上没有 12V 电源只有一个 5V 适配器于是用升压板把 5V 抬到 9V 到 12V 再喂给 L298N 的 Vs。这个方案能跑但有两个前提必须满足否则就是白折腾。第一是升压板的电流能力。直流减速电机堵转时的瞬时电流可能是额定值的 3 到 5 倍一个标称 1A 的小升压板遇到堵转直接掉压保护单片机那边看到的就是电压跌落甚至复位。选型时按两路电机额定电流之和再乘 2去留余量别按标称值卡着用。第二是输出电压别贪高9V 到 12V 对大多数 12V 减速电机够用了抬太高只会让 H 桥发热更严重。共地这件事必须单独说。L298N 中间那个 GND 要引出两条线一条接 STM32 的 GND一条接电源负极。少了 STM32 这一路IN1到IN4的高电平在 L298N 看来没有参考点表现就是电机时转时不转、按键按下去轮子抽一下属于典型的看着像代码 bug其实是线没接。提示所有地必须在一点汇合别让电机电流从 STM32 的 GND 引脚上淌过去。做法是电源负极、L298N 的 GND、STM32 的 GND 三者接在一个端子上电机大电流走粗线信号地走细线。原文main.c里有一句delay_ms(10000)初始化完电机之后先干等十秒。这十秒大概率是作者给升压板软启动和电源稳定留的观察窗口也可能是调试时用来判断到底是没初始化还是没上电。放到正式工程里应该删掉或者换成按键触发的上电自检——总不能每次复位都让机器人在原地站十秒。3. 代码骨架把 H 桥真值表翻译成 PA2–PA5 的推挽输出接线定下来之后代码其实只有三件事把四个脚配成推挽输出并给一个确定的初始电平、把 H 桥的真值表封装成六个函数、在主循环里根据键值调用它们。整套逻辑不到一百行但每一行都有讲究。3.1 motor.c 的初始化时钟、输出模式和上电默认电平先看初始化函数。标准外设库的套路是先开时钟再配引脚顺序反了配置不生效这是新手最容易漏的一步。#include motor.h #include stm32f10x.h #include stm32f10x_rcc.h #include delay.h void Motor_12_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 1. 先开 GPIOA 的 APB2 时钟不使能时钟后面所有配置都是空写 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* 2. 四个方向脚一次配完PA2/PA3 管电机APA4/PA5 管电机B */ 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); /* 3. 上电先给一个确定电平避免复位瞬间引脚悬空导致电机乱动 */ GPIO_ResetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5); }GPIO_Mode_Out_PP选推挽而不是开漏是因为 L298N 的输入脚内部没有上拉开漏输出高电平时靠外部电阻拉高边沿会软长线传输时容易误判。推挽能主动输出高电平驱动能力足够属于这里的标准选择。GPIO_Speed_50MHz这个参数经常被无脑填最高其实它控制的是输出驱动器的翻转速率也就是边沿陡峭程度。电机方向信号的变化频率顶多几百赫兹2MHz 完全够用。填 50MHz 会让边沿变陡、高频谐波变强在电机这种感性负载旁边反而更容易串扰。不算大错但如果后面要过 EMI 或者旁边有 ADC 采样把它降到 2MHz 会更安分。最后那句GPIO_ResetBits把所有 IN 脚拉低很多人以为这是停止其实要看模块的真值表。常见 L298N 模块在IN1IN20时两个输出端会同时被拉到低电平电机绕组被短接形成制动而不是断电。真正要松开车轮让它自己滑行得把 ENA 或 ENB 拉低关断整路输出。这个区别在做小车转弯、下坡的时候很关键想让车滑行却用了刹车逻辑轮子会一直拖着地。3.2 正反转与停止IN1/IN2 的四种组合到底对应什么六个控制函数本质上是同一张真值表的六种填法/* 电机A 停止两个输入同为高输出端被拉到同一电平绕组短接制动 */ void Motor_1_STOP(void) { IN1(High); IN2(High); } /* 电机A 正转IN1 低、IN2 高OUT1/OUT2 之间形成正向压差 */ void Motor_1_PRun(void) { IN1(Low); IN2(High); } /* 电机A 反转把上面两个电平对调即可 */ void Motor_1_NRun(void) { IN1(High); IN2(Low); } /* 电机B 完全对称只是换到 IN3/IN4 */ 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); }正转和反转之间唯一的区别就是两个电平对调物理上就是把加在电机两端的电压极性反过来电机自然反向旋转。这比用继电器换向省事得多也没有机械寿命问题。停止用同高而不是同低这在模块上对应的是两个输出都被拉到高电平同样是短接制动只是短接的参考点不同。两种在制动效果上差别不大真正有区别的是完全不动作——那需要拉低使能脚。如果你的应用里有松手后让轮子自由滚动的需求就得在硬件上把 ENA/ENB 的短接帽拔掉接到一个 GPIO 或者 PWM 通道上停下来的时候把使能拉低。还有一个容易忽略的细节电机从正转直接切到反转中间没有停顿。这时电机的反电动势和电源叠加瞬时电流可能是正常值的两倍以上电源弱一点就直接把单片机拉复位。稳健的做法是在换向之前插 50 到 100 毫秒的停止让反电动势先泄掉。这套示例代码里每个 case 结尾只有一个delay_ms(1)明显不够属于能跑但别在真车上这么用的写法。3.3 motor.h 里的宏能用但埋了两个雷motor.h用宏把GPIO_SetBits/GPIO_ResetBits包装成带参数的写法#define High 1 #define Low 0 #define IN1(a) if (a) \ GPIO_SetBits(GPIOA, GPIO_Pin_2); \ else \ GPIO_ResetBits(GPIOA, GPIO_Pin_2)这套宏读起来很省事IN1(High)就是拉高但它在 C 语言里属于典型的能用但危险写法两个雷点第一个是悬挂 else。宏展开后是一整条if ... else ...语句没有用do { } while(0)包起来。如果外层写成if (cond) IN1(High); else 其他操作;展开之后那个else会和宏内部的if配对编译器要么报错要么行为完全错乱。目前六个函数都是单条独立调用所以没暴露但只要有人把它塞进条件分支里立刻翻车。第二个是参数没有括号保护。宏参数a直接进if (a)传入x 1这种表达式看着没事传x 1就变成赋值编译器还不一定报错。我的习惯是直接换成内联函数代价是几行代码换来的是可调试、可断点、不会展开出惊喜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在 ARMCC 下会被内联展开生成的机器码和宏完全一样不会有函数调用开销但保留了类型检查和作用域。这套代码是教学示例宏写法能看懂就行真要用到项目里我一般会花十分钟把它改掉——后期加 PWM、加编码器、加状态机的时候宏带来的麻烦远大于省下的那点打字时间。顺带说一句key.h里#include sys.h这件事。这个头文件来自某个厂商的标准库工程模板里面封装了位带操作之类的宏。如果你的工程里没有这个文件编译会直接报找不到头文件解决方式不是去网上乱下一份sys.h而是把KEY0、KEY1、WK_UP这三个宏改成直接调用GPIO_ReadInputDataBit一行一个反而更清楚。4. 按键那一层KEY_Scan 的优先级、消抖和 C8T6 上没有的 GPIOE电机能转之后下一步是让它听指挥。这套代码用三个按键做主控一个正转、一个反转、一个停止。逻辑不复杂但key.c里藏着这套资源最容易翻车的地方而且翻得很有迷惑性——编译零警告下载成功运行起来却完全不是那么回事。4.1 C8T6 上没有 GPIOEKEY0/KEY1 必须换脚先把最大的问题挑明。这套代码的按键部分用的是GPIOE的Pin_3和Pin_4而标题写的是 STM32F103C8T6。C8T6 是 LQFP48 封装引出的端口只有 PA0 到 PA15、PB0 到 PB15、PC13 到 PC15根本没有 GPIOE。GPIOE 是 100 脚以上型号才有的东西正文里出现stm32f103zet6那个型号才有 PE 口两处对不上。后果是什么标准外设库的头文件是按整个系列定义的RCC_APB2Periph_GPIOE和GPIOE这些符号在 C8T6 的工程里照样能编译通过因为编译器只认寄存器地址不管芯片上有没有这个端口。运行起来对 GPIOE 寄存器的写操作全落进未映射的地址空间读回来是随机值或者恒零。表现就是KEY0宏读到的值一直是 0KEY_Scan认定按键一直被按着主循环不停执行正转分支电机一上电就自己转你按什么键都没反应。解决办法不复杂把两个按键挪到 C8T6 真实存在的引脚上比如PB0和PB1/* key.h把端口和引脚一起换掉 */ #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) /* 这个脚 C8T6 上有保留 *//* key.c时钟和引脚同步修改 */ void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); /* KEY0 / KEY1上拉输入按键另一端接地按下读到 0 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOB, GPIO_InitStructure); /* WK_UP下拉输入按键接 3.3V按下读到 1 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; GPIO_Init(GPIOA, GPIO_InitStructure); }注意原文key.c里还有一处硬伤先写GPIO_InitStructure.GPIO_Pin GPIO_Pin_4|GPIO_Pin_3;下一行又写GPIO_InitStructure.GPIO_Pin GPIO_Pin_5;把前面那句覆盖掉了最后真正被初始化的只有Pin_5Pin_3和Pin_4从头到尾没配过停在复位后的浮空输入状态。就算芯片上有 GPIOE按键也是失效的。这种前一行被后一行吃掉的 bug 在结构体配置里特别常见编译器不会提醒你只能靠逐行核对。WK_UP那一路上拉/下拉的方向别搞反接地的按键配上拉输入接电源的按键配下拉输入。反了的话按键永远是同一个电平KEY_Scan会认为按键一直处于按下状态。4.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); /* 软件消抖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; }mode这个参数控制的是按住不放会不会连续触发。传 0 的时候按下一次只返回一次键值之后必须三个键全松开才恢复适合按一下切换一个状态的场景。传 1 的时候每次调用都把key_up置回 1按住就会不停地返回键值适合做点动。这套代码主循环里传的是 0所以是按一下动一下。delay_ms(10)这十毫秒是机械按键的抖动窗口。轻触开关在按下和松开的瞬间会有几毫秒的接触弹跳不消抖的话一次按压会被识别成好几次。10ms 是经验值大多数轻触开关够用如果用的是手感偏软的按键或者引线比较长可以加到 20ms。代价是主循环被阻塞按键响应变慢——对于这种纯按键控制无所谓但如果后面要加串口接收或者传感器采样就得把消抖改成非阻塞的定时器轮询。优先级也要留意if (KEY0 0)排在最前面意味着三个键同时按下时只会返回KEY0_PRES。正常操作不会同时按但手滑的时候会出现按了正转却反转。如果这个顺序不合你的直觉把if链的顺序调一下就行逻辑上没什么必须。还有一个隐藏的坑WK_UP那一路是低电平有效还是高电平有效取决于硬件接法。代码里写的是WK_UP 1表示按下对应下拉输入接 3.3V 的接法。如果你的板子上这个键是接地的那种条件就得改成 0不然表现就是不按的时候一直在触发正转。4.3 从点动到自锁一个状态变量的事这套代码的控制方式是按住转、松手停吗不是。按键返回一次键值主循环调用一次Motor_1_PRun()之后 GPIO 的输出电平就一直保持在那里松手了电机照样转。这是 GPIO 输出寄存器的特性你写进去什么它就保持什么跟你后面还按不按键没关系。所以实际行为是按一下正转一直转按一下停止才停。这个特性对小车来说正好够用。但如果想要按一下正转、再按一下同一个键就停的切换效果就得自己维护状态/* 0停止 1正转 2反转用静态变量跨循环保持 */ static uint8_t run_state 0; switch (key) { case WKUP_PRES: /* 同一个键做正转/停止切换 */ run_state (run_state 1) ? 0 : 1; break; case KEY1_PRES: /* 反转键已经是反转就停 */ run_state (run_state 2) ? 0 : 2; break; case KEY0_PRES: /* 停止键无条件清零 */ run_state 0; break; } /* 统一根据状态输出避免在多个分支里重复写电机函数 */ switch (run_state) { case 1: Motor_1_PRun(); Motor_2_PRun(); break; case 2: Motor_1_NRun(); Motor_2_NRun(); break; default: Motor_1_STOP(); Motor_2_STOP(); /* 注意这里是制动不是滑行 */ break; }把按键和执行拆成两层之后后面加限位开关、加遥控指令、加自动巡航都只需要改状态变量不用动电机函数。这也是这套示例代码最值得吸收的地方——六个电机函数是干净的原子操作怎么组合是上层的事。5. 避坑与排查不转、只抖、方向反了、上电乱撞的 5 个现场这套代码我在不同板子上复现过几次出问题的位置高度集中。下面五条按现象 → 原因 → 解决整理基本都是接线和配置层面的跟算法无关。5.1 上电电机自己转按键完全没反应现象程序下载进去一上电两个轮子就开始转按任何按键都没变化示波器量PA2到PA5也确实有电平变化。原因按键读到的值恒为按下。两个可能叠加在一起——一是前面说的 C8T6 上没有 GPIOEKEY0读到 0二是key.c里GPIO_Pin被后一行覆盖Pin_3、Pin_4根本没配成上拉输入停在浮空状态读回来的值随机。浮空输入在没接外部上拉时读数由分布电容和干扰决定时不时就是 0。解决先确认芯片型号和端口是否匹配把GPIOE全部换成GPIOB再逐个检查结构体赋值一个GPIO_InitStructure只配一组同属性的引脚配完立刻GPIO_Init不要复用同一个结构体反复改字段。改完用调试器在KEY_Scan里打个断点看一眼KEY0的实际读值比猜快得多。5.2 轮子不转只有轻微的嗡声和抖动现象电机通电但转不起来手拨一下能转半圈松手又停。万用表量 L298N 输出端电压只有 2 到 3V。原因供电电压太低。用 5V 直接喂 L298N 的 Vs 端达林顿结构吃掉 1.4V 到 3V 的压降电机两端剩不下几伏启动转矩根本不够。原文作者用升压板就是为了绕开这个但如果升压板功率不够堵转瞬间掉压表现是一样的。解决量 Vs 端在电机启动瞬间的电压如果明显低于设定值说明电源或升压板带不动。先脱开电机空载量再挂电机量两次对比就能定位。选电源时按电机额定电流的 2 到 3 倍留余量别按空载电流算。5.3 电机一转单片机就复位或者程序跑飞现象空载时一切正常轮子一转串口打印乱码或者程序从头开始跑。原因电机启动电流把电源拉垮或者大电流从 STM32 的 GND 线上淌过去造成地电位跳动。还有一个隐蔽的原因是从 L298N 的5V端子给单片机供电电机一启动板载稳压器输出跟着抖。解决电机电源和逻辑电源分开走只在一点共地单片机用自己的 USB 或者独立稳压供电在电机两端并一个 0.1μF 瓷片电容加一个续流二极管压一压换向时的尖峰。如果用的是升压板检查它的输入线是不是太细。5.4 两个轮子转向相反或者前进变成了后退现象按下正转键左轮往前右轮往后小车原地打转。原因电机是随便接的OUT1/OUT2和实际转向没有约定。两个电机的机械安装方向也可能是镜像的。解决不用改代码把转向不对的那一路电机的两根线对调。如果想在软件里解决把对应那一路的PRun和NRun里两个电平对调即可。我一般倾向于动线不动代码因为代码里正转应该对应小车前进把硬件的镜像关系在接线阶段就校正过来后面写运动逻辑会清晰很多。5.5 拔了跳线帽接 PWM占空比怎么调转速都不变现象为了调速把 ENA、ENB 的短接帽拔了接到 PWM 输出上程序里改比较值轮子还是全速转。原因两个方向。一是ENA/ENB那根线其实还短接着或者接到了固定的高电平二是 PWM 的 GPIO 没有配成复用推挽输出GPIO_Mode_AF_PP配成了普通推挽定时器根本没接管这个引脚。解决先用万用表量 ENA 脚在占空比 50% 时的平均电压如果是 2.5V 左右说明 PWM 出来了问题在模块侧如果一直是 5V 或者 0V问题在配置侧。检查GPIO_InitStructure.GPIO_Mode是不是写成了GPIO_Mode_AF_PP以及对应定时器的时钟有没有使能。这一步的细节在下一章展开。6. 从能转到能控PWM 调速的频率、死区与堵转电流怎么定方向控制跑通之后绝大多数人会立刻想要调速。硬件上只需要拔掉 ENA/ENB 的短接帽把这两个脚接到定时器的 PWM 通道上软件上多一个定时器初始化主循环里把固定的Motor_1_PRun()换成先定方向再改占空比。/* TIM3_CH1(PA6) / TIM3_CH2(PA7)1kHz PWM分辨率 1000 级 */ void Motor_PWM_Init(void) { 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); /* 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); /* 72MHz / (711) / (9991) 1kHz */ TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; 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_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse 0; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE); }频率怎么定看下面这张表。L298N 的开关响应在微秒量级频率拉太高会出现占空比给了但输出没跟上的情况PWM 频率现象建议100Hz 以下电机有明显抖动和嗡嗡声低速不稳不推荐1k 到 5kHz有轻微啸叫转矩平稳L298N 发热正常推荐区间10kHz 以上超出人耳但开关损耗上升模块明显变热短时可用20kHz 以上边沿跟不上占空比失真效率下降不建议真正让人困惑的是起步死区——占空比从 0 慢慢往上加加到 20% 甚至 30% 轮子还不转一过某个点突然窜出去。这不是 H 桥的死区那个是防止上下管直通的L298N 内部逻辑已经处理而是电机的静摩擦和减速箱阻力造成的启动门槛。补偿办法很简单在代码里给一个起步偏置/* 目标占空比 0-1000低于启动门槛时直接抬到门槛值 */ #define START_OFFSET 250 static void Motor_SetDuty(uint16_t duty) { if (duty 0) { TIM_SetCompare1(TIM3, 0); /* 真停不要偏置 */ return; } if (duty START_OFFSET) duty START_OFFSET; /* 一给就走避免低速不动 */ TIM_SetCompare1(TIM3, duty); }最后说电流。拿万用表串在 Vs 和电源之间空载量一次手捏住轮子模拟堵转再量一次两个数字差多少心里就有数了。常见 12V 减速电机空载 100 到 200mA堵转能到 1A 以上而 L298N 模块在这个电流下跑几分钟外壳就烫手。持续跑之前一定贴散热片或者把两路电机的占空比错开别让它们同时满负荷。血泪经验是先量电压再改代码。我见过太多人包括我自己对着motor.c逐行怀疑人生最后发现是升压板带不动、地线没接、或者 ENA 的短接帽还在上面。从那以后我每次接完线都强制走一遍量 Vs 空载电压 → 量共地通断 → 手动短接 ENA 看轮子转不转三步过了再打开编译器。这套代码文件不多motor.c、motor.h、key.c、key.h加上main.c就是全部拿到之后按第 4 章把引脚换成 C8T6 真实存在的口剩下的直接编就能跑。希望帮到你。本文还有配套的精品资源点击获取