简介这是一份面向嵌入式初学者与单片机课程实践者的实战参考资料围绕 STM32F103C8T6 主控搭配 L298N 双 H 桥驱动模块控制两台直流电机聚焦正转、反转与停止三种基本动作的实现问题属于入门级电机驱动与 GPIO 控制练习。资源包共 1 个文件为 PDF 格式的图文笔记约 6.97MB内容依次覆盖器件清单、硬件接线与代码分析三部分。硬件部分说明 OUT1OUT4 与两台电机的对应关系、IN1IN4 分别接入 PA2PA5 的控制方式、12V 端口与公共 GND 的接法并特别提示 5V 端口不可接电源、升压板供电的注意事项。代码部分给出 main.c 的按键扫描主循环、motor.c 的 GPIO 推挽输出初始化以及停止、正转、反转六组电平组合函数并附 motor.h 中宏定义的引脚封装写法可直接对照移植。已有 939 人学习适合用来快速理清 L298N 与 STM32 的联调思路、排查电机不转或方向相反等常见现象。1. 按键按下电机只响不转这套 STM32F103C8T6 驱动 L298N 的代码到底管哪一段小车底盘装好、轮子拧紧、STM32F103C8T6 最小系统板插上 USBL298N 的 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、5V12V 是电机电源输入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 方向控制位 1PA3IN2电机 A 方向控制位 2PA4IN3电机 B 方向控制位 1PA5IN4电机 B 方向控制位 2GNDGND中间端子逻辑电平参考地必须接—OUT1 / OUT2接电机 A 两根线—OUT3 / OUT4接电机 B 两根线—ENA / ENB保留跳线帽全速使能选 PA2 到 PA5 不是随便挑的。这四个脚在 C8T6 上都是普通 GPIO推挽输出没问题。但它们有第二功能PA2/PA3 是 USART2 的 TX/RXPA4/PA5 是 SPI1 的 NSS/SCK。你如果同时要接串口调试或者 SPI 外设就得换脚或者接受复用冲突。我一般会把这四个脚留给电机串口挪到 PA9/PA10SPI 挪到 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); /* 上电默认全部拉低IN1IN20电机处于自由停车状态 */ 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 的真值表IN10、IN20 对应的是自由停车电机两端悬空转子靠惯性滑行。但要注意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 读到 0WK_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。这段代码是从大容量芯片比如 ZET6144 脚的工程里搬过来的引脚定义没改。开 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 接 ENBIN1~IN4 保持原样。代码上加一段 PWM 初始化。/* TIM3_CH1(PA6) / TIM3_CH2(PA7) 输出 PWM频率 72M / (psc1) / (arr1) */ 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决定频率。取 psc71、arr999得到 72MHz / 72 / 1000 1kHz这个频率下电机听不到啸叫调速也够线性。频率太低电机会抖太高了 L298N 的开关损耗上来发热明显。GPIO_Mode_AF_PP这一行写错成Out_PP是最常见的翻车点写错了 PWM 出不来引脚就是普通 GPIO 电平。再往下改就是把六个散函数收进一个结构体方向、速度、目标速度放一起主循环里跑一个简单的状态机。这样后面加编码器、加 PID 的时候接口不用推倒重来。我现在的习惯是任何一套从按键开始的控制代码第一次跑通之后都强制走一遍「加一路 PWM、测一次最低启动占空比」。因为电机的死区电压因型号而异有的 20% 才转有的 10% 就动了这个数不測出来后面写闭环全是空中楼阁。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
双足机器人设计:自由度配置、扭矩估算与步态验收 简介:这份文档面向双足机器人控制与嵌入式开发学习者,围绕北京汉库HGR-3HV-C型号,系统讲解步伐源代码的组织与运动控制思路,适合需要理解关节协同、步态调试与程序优化的研发人员参考。压缩包仅含1个doc文件,约9.07MB&… · 2026/11/3 13:52:11
MLOps实践指南:从模型开发到生产部署 1. 为什么我们需要MLOps?当机器学习项目从实验室走向生产环境时,数据科学家们常常会遇到一个令人头疼的现象:在测试集上表现优异的模型,一旦部署到真实业务场景中,性能就会大幅下降。这种现象被戏称为"模型漂移&q… · 2026/11/3 16:50:53
AI Agent错误处理与工程化实践:从能跑到扛得住 做Agent开发的朋友应该都有同感:写一个能跑通的Demo不难,难的是让它稳定、可控、出了问题还能快速定位。我见过太多项目卡在“本地能跑、上线就妖”的阶段,本质问题往往不在模型能力,而在错误处理和工程化基建没有跟上。这篇是Age… · 2026/11/3 18:16:01
AI测试工程师转型指南:从传统测试到智能测试 1. AI浪潮下的测试工程师转型抉择2026年的软件测试行业正经历一场前所未有的技术革命。作为一名在测试领域摸爬滚打十年的老兵,我亲眼见证了从纯手工测试到自动化测试,再到如今AI驱动的智能测试的演进过程。最近三个月,我面试了37位测试工程师… · 2026/11/4 10:29:04
GROMACS分子动力学全流程:构建、平衡、轨迹分析与论文方法 简介:面向分子动力学模拟初学者,以及需要撰写论文方法章节的研究生,这份文档把从体系构建到数据分析的完整流程拆成六个环节逐一讲解:加溶剂、加离子以平衡电荷;能量最小化,避免原子间距过近而碰撞… · 2026/11/4 10:29:04
ANSYS许可管理器彻底卸载指南与深度清理技巧 1. 为什么需要彻底卸载ANSYS许可管理器 ANSYS作为一款功能强大的工程仿真软件,其许可管理器(ANSYS License Manager)在安装后往往会深度集成到系统环境中。常规的卸载方式经常会出现以下问题: 残留的注册表项导致新版本安装失败 … · 2026/11/4 10:29:04
Superpowers:本地化AI开发增强层的技术原理与工程实践 1. “Superpowers”不是超能力,而是开发者工具链的智能增强层 最近在好几个技术社区里,看到“superpowers”这个词被高频提及,尤其和 Claude Code、Antigravity、Codex CLI、Cursor 这几个名字绑在一起。很多人第一反应是:“这是某… · 2026/11/4 10:29:04
别再误解了!直流无刷电机AX、BY、CZ是按120°电角度划分的 有人拿着电机问我:直流无刷电机里AX、BY、CZ到底怎么划分?是不是像直角坐标那样,按垂直轴线把三相绕组分开的?这个问题听起来基础,但真把它完全想明白,得把绕组标号、电角度、坐标变换串在一起捋一遍。我当… · 2026/11/4 10:29:04
实用网站筛选逻辑与使用习惯:告别收藏夹吃灰 1. 从“收藏夹吃灰”说起:实用网站到底该怎么选我见过太多人的浏览器收藏夹,里面躺着几百个“实用网站”,从在线工具到资源导航,从效率神器到素材仓库,结果一年点开的次数一只手数得过来。问题不在于网站本身不好&… · 2026/11/4 10:28:03
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38