STM32F103驱动L298N电机:接线、PWM与C语言代码实战

发布时间:2026/8/31 17:12:28
STM32F103驱动L298N电机:接线、PWM与C语言代码实战 简介本资源是一套基于C语言开发的L298N电机驱动程序专为正点原子STM32F103核心板设计面向嵌入式初学者与机器人开发爱好者解决直流双电机的正反转控制、差速拐弯及PWM无级调速等典型运动控制问题。压缩包共76个文件含33个头文件.h用于外设定义与函数声明、32个源文件.c实现GPIO初始化、PWM配置、电机方向逻辑与调速接口另有Keil工程配置.uvprojx/.uvoptx、启动代码、系统库及LICENSE等支撑文件整体体积仅312KB结构清晰、模块分明便于理解底层驱动逻辑与工程组织方式。已有125人下载学习读者可直接导入Keil MDK编译运行完整掌握STM32 GPIO推挽输出控制H桥、TIM定时器生成PWM信号、双电机协同调速等关键技能并复用核心函数快速构建智能小车底盘控制层。 说起来你可能不信我第一次用正点原子F103核心板驱动L298N的时候烧的不是电机驱动模块而是差点把车上的蓝牙模块、舵机和屏幕全部带崩。原因很简单电源接法错了L298N的逻辑供电和电机供电共用一个电池然后核心板又从L298N的5V输出口取电电机一启动整个板子瞬间欠压复位。从那以后我才认真把L298N的接线、PWM频率、代码架构全部重新捋了一遍。这篇博文就是把这些东西整理出来给正准备做智能小车、麦克纳姆轮底盘或者简单机械臂的同学一个能直接照抄的C语言驱动方案。这个方案围绕三个核心点展开L298N怎么接才稳、STM32F103的定时器PWM怎么配置才不抖、C语言驱动代码怎么组织才能让正反转、拐弯和调速都干净利落。无论你是刚学完寄存器点亮LED的新手还是已经在用HAL库做项目的进阶玩家都能从中拿到一套可以直接落到开发板上的代码思路和排查经验。1. 接线就是一半的坑L298N与F103核心板的连接细节很多人拿到L298N模块第一反应是这不就是一个直流电机驱动板嘛IN1接高、IN2接低就正转反过来就反转多简单。这句话没错但真正落地到正点原子F103核心板上至少有四个细节没注意到的人肯定会翻车。1.1 先认清L298N模块上的电源和逻辑接口L298N模块的核心芯片是ST的L298内部是两个H桥每个H桥可以驱动一路直流电机或者一个双极四线步进电机。模块上常见的接口从左到右大致是12V供电输入VS、GND、5V输出、ENA跳帽、IN1、IN2、IN3、IN4、ENB跳帽以及两路电机输出端OUT1/OUT2、OUT3/OUT4。这里最关键的区分是VS和VSS。VS是电机电源也就是电机转动的能量来源一般接7V到12V我实际用过6V到24V都能工作但超过12V后L298本身的压降和发热会明显增大。VSS是逻辑电源给芯片内部逻辑电路供电L298N模块上通常已经用一块78M05稳压芯片把VS降压成5V所以模块上那个5V输出口是从这里出来的。你不需要单独再给逻辑部分供电除非你把VS接到了很低的电压导致78M05无法正常稳压。然后是ENA、ENB这两个跳帽。跳帽短接时该路电机始终使能只要IN1/IN2电平确定电机就全速转。想要PWM调速必须拔掉对应的跳帽把单片机的PWM信号接到ENA或ENB引脚上。这一点特别容易被忽略很多人写了PWM代码但电机还是全速转查半天发现跳帽没拔。1.2 F103核心板的引脚用哪些正点原子F103核心板不管是最小系统板还是战舰核心板的引脚是全部引出的理论上只要你愿意任何能输出高低电平的GPIO都可以接IN1~IN4。但PWM引脚就不能随便选了必须接到定时器的输出通道上。F103的定时器资源里TIM1和TIM8是高级定时器TIM2/3/4是通用定时器都能输出PWM。我建议优先用TIM3的CH1和CH2对应PA6和PA7。原因是PA6、PA7在最小系统板上很好引出而且TIM3的配置不像TIM1那样还要处理刹车、互补输出这些额外逻辑对初学者更友好。具体接线表如下功能引脚说明PWM左轮PA6 (TIM3_CH1)接L298N的ENAPWM右轮PA7 (TIM3_CH2)接L298N的ENB左轮方向1PB0接IN1左轮方向2PB1接IN2右轮方向3PB2接IN3右轮方向4PB3接IN4如果你的板子PA6、PA7被其他功能占用了也可以改到PB0、PB1TIM3_CH3、TIM3_CH4或者PA8、PA9TIM1_CH1、TIM1_CH2但改引脚时要注意对应定时器通道和复用功能配置别想当然地改个GPIO编号就以为能输出PWM。1.3 共地是所有信号正常工作的前提这一点我放在接线部分里单独说因为太重要了。F103核心板的GND必须和L298N模块的GND连在一起。如果不共地单片机输出的0V和L298N逻辑部分的0V不是同一个参考点结果就是电机要么不转要么乱转PWM调速完全失灵用万用表量信号线电压也时高时低。实际接法是这样的锂电池正极接L298N的12V输入负极接L298N的GND然后用一根线把L298N的GND和F103核心板的GND连起来。核心板的电源可以来自L298N的5V输出但我个人不推荐在大功率电机项目里这样取电因为电机启动瞬间电流冲击很大78M05的输出电压会跟着波动单片机容易复位。更稳的做法是核心板用独立的USB线或者单独的小容量锂电池供电只跟L298N共地。1.4 电机电源和逻辑电源的关系很多刚入门的同学以为L298N那个5V输出能给单片机供电同时又能给逻辑电路供电这不是不行但要分场合。如果电机是小功率减速电机空载电流在100mA以内堵转电流几百毫安那么从L298N的5V输出给核心板供电勉强能用。但如果电机稍微带点负载比如小车在粗糙地面跑或者麦克纳姆轮那种摩擦力很大的轮子我建议还是把核心板独立供电。顺带一提L298N模块在电机供电端最好加一个大电容我习惯在电源输入两端并一个470uF电解电容和一个104瓷片电容。电解电容能吸收电机启停和堵转时的大电流波动瓷片电容用来滤高频干扰。别小看这两个电容省了它们单片机和蓝牙模块被复位、乱码的概率能翻好几倍。2. PWM调速先算清楚定时器频率、占空比与引脚映射PWM调速的原理不复杂给电机两端一个方波电压通过改变高电平占空比来改变平均电压电机转速就跟着变了。但STM32F103的PWM配置涉及时钟树、预分频、自动重装值、比较值这一整套寄存器逻辑很多人卡在这一步。我先把计算过程完整走一遍。2.1 一个公式解决所有频率计算F103的定时器时钟来自APB1总线默认是72MHz如果你的工程用的8MHz外部晶振并且配置正确。定时器输出PWM的工作频率由预分频器PSC和自动重装值ARR共同决定PWM频率 72MHz / ((PSC 1) × (ARR 1))占空比则由比较寄存器CCR决定计算公式占空比 CCR / (ARR 1)举个例子我想要1kHz的PWM频率可以选PSC 71ARR 999。代入公式(711) × (9991) 72 × 1000 7200072MHz除以72000正好是1000Hz。此时ARR1 1000也就是CCR从0写到999对应占空比0%到100%。2.2 电机驱动的PWM频率到底选多少这是我踩过坑之后专门研究过的问题。PWM频率太低比如几十Hz电机转起来会有肉眼可见的顿挫声音也很尖频率太高比如30kHz以上L298的开关损耗会急剧增加模块发热非常厉害而且电机电感对高频分量的阻抗大反而导致有效电压上不去。L298的手册里给出的一个合理工作区间是1kHz到25kHz我实践下来最舒服的是1kHz到10kHz。如果想省事直接用1kHz因为频率低定时器中断和GPIO翻转的压力小模块发热也低电机啸叫在可接受范围内。如果你的电机本身有减速齿轮箱齿轮噪音可能比PWM啸叫还大那用5kHz或者10kHz也完全没问题。我最终在这个项目里用了1kHz也就是PSC 71ARR 999。这样CCR的取值可以做到0~999速度值映射也很直观你代码里存一个0~100的百分比速度值写进CCR的时候乘以10就行几乎没有额外运算量。2.3 HAL库的PWM初始化完整代码工程基于STM32CubeMX生成的HAL库框架核心在于三点GPIO复用配置、定时器基础配置、PWM通道配置。下面这份代码可以直接复制到项目的tim.c或者单独放在motor.c里。// motor.c 中的PWM初始化部分 #include motor.h static TIM_HandleTypeDef htim3; void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef* htim) { GPIO_InitTypeDef GPIO_InitStruct {0}; if (htim-Instance TIM3) { __HAL_RCC_TIM3_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); // PA6 - TIM3_CH1PA7 - TIM3_CH2 GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PB0~PB3 用作普通推挽输出控制方向 GPIO_InitStruct.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLDOWN; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); // 初始状态四个方向引脚全部拉低电机不转 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3, GPIO_PIN_RESET); } } void Motor_Init(void) { htim3.Instance TIM3; htim3.Init.Prescaler 71; // 72MHz / 72 1MHz htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 999; // 1MHz / 1000 1kHz htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim3); TIM_OC_InitTypeDef sConfigOC {0}; sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 0; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); }要注意的是Pulse字段就是CCR的初始值。我习惯把它设成0也就是占空比为0电机不会在初始化瞬间猛转。2.4 如果你用寄存器或者标准外设库我知道还有不少人在用正点原子老教程里的标准外设库或者干脆直接寄存器操作。这里给一份PWM配置的最简寄存器版思路方便你对照理解HAL库到底做了什么// 以TIM3为例寄存器方式配置PWM RCC-APB1ENR | RCC_APB1ENR_TIM3EN; // 使能TIM3时钟 RCC-APB2ENR | RCC_APB2ENR_IOPAEN; // 使能GPIOA时钟 GPIOA-CRL ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6 | GPIO_CRL_CNF7 | GPIO_CRL_MODE7); GPIOA-CRL | GPIO_CRL_CNF6_1 | GPIO_CRL_MODE6 | GPIO_CRL_CNF7_1 | GPIO_CRL_MODE7; // 上面两行把PA6、PA7配置为复用推挽输出最大速度50MHz TIM3-PSC 71; // 预分频 TIM3-ARR 999; // 自动重装 TIM3-CCMR1 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; // PWM模式1 TIM3-CCMR1 | TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_2; TIM3-CCER | TIM_CCER_CC1E | TIM_CCER_CC2E; // 使能输出 TIM3-CR1 | TIM_CR1_CEN; // 启动定时器 // 设置占空比 TIM3-CCR1 500; // 50%占空比 TIM3-CCR2 300; // 30%占空比很多奇怪的问题比如PWM没输出、波形毛刺大都可以从这份寄存器版本里看出端倪。如果你用HAL库调不通先把寄存器版本的代码烧进去再用示波器看PA6波形基本能定位是GPIO复用没配好还是定时器时钟没开。3. 驱动代码怎么写方向控制、速度控制与模块化接口讲完硬件和定时器接下来是C语言驱动代码的架构设计。我见过不少人把方向控制逻辑直接写在main函数的按键判断里那样写出来的程序功能能跑但一旦要加蓝牙遥控、循迹、避障代码会越改越乱。我的建议是封装成一个独立的motor驱动模块用头文件暴露几个干净的接口底层细节全部藏起来。3.1 驱动层应该提供哪些接口在设计一个电机驱动模块前先想清楚上层控制代码需要什么。以这个项目为例我需要初始化电机、设置左轮速度、设置右轮速度、前进、后退、左转、右转、原地旋转、停止。速度的取值范围我用0到100的百分比这样上层逻辑不需要关心ARR和CCR的细节。头文件我这样写// motor.h #ifndef __MOTOR_H #define __MOTOR_H #include stm32f1xx_hal.h #define MOTOR_SPEED_MAX 100 #define PWM_ARR_VALUE 999 typedef enum { DIR_STOP 0, DIR_FORWARD, DIR_BACKWARD } Motor_Dir_t; void Motor_Init(void); void Motor_SetSpeed(uint8_t leftSpeed, uint8_t rightSpeed); void Motor_Forward(uint8_t speed); void Motor_Backward(uint8_t speed); void Motor_TurnLeft(uint8_t speed); void Motor_TurnRight(uint8_t speed); void Motor_SpinLeft(uint8_t speed); void Motor_SpinRight(uint8_t speed); void Motor_Stop(void); #endif3.2 方向控制的底层逻辑L298N的方向控制其实就是一个真值表。以左轮为例IN1接PB0IN2接PB1IN1 (PB0)IN2 (PB1)电机状态00刹车/停止01反转10正转11刹车急停很多新手会犯的错是把IN1、IN2都设为1当作停止这个状态其实是L298的快速制动电机会瞬间堵转电流冲击很大长期使用容易烧模块。停止状态下我更推荐把两个引脚都设成0。虽然这时候电机因为惯性还会滑行一点但对于小车来说更安全模块发热也小。方向控制的代码实现// motor.c 中的方向控制 static void Motor_SetLeftDir(Motor_Dir_t dir) { switch (dir) { case DIR_FORWARD: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); break; case DIR_BACKWARD: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); break; case DIR_STOP: default: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0 | GPIO_PIN_1, GPIO_PIN_RESET); break; } } static void Motor_SetRightDir(Motor_Dir_t dir) { switch (dir) { case DIR_FORWARD: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_RESET); break; case DIR_BACKWARD: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_SET); break; case DIR_STOP: default: HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2 | GPIO_PIN_3, GPIO_PIN_RESET); break; } }3.3 速度控制与归一化映射速度控制的核心是把0~100的百分比速度映射到CCR的0~999。最简单的做法是线性映射static uint16_t SpeedToCCR(uint8_t speed) { if (speed MOTOR_SPEED_MAX) speed MOTOR_SPEED_MAX; return (uint16_t)((uint32_t)speed * PWM_ARR_VALUE / MOTOR_SPEED_MAX); } void Motor_SetSpeed(uint8_t leftSpeed, uint8_t rightSpeed) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, SpeedToCCR(leftSpeed)); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, SpeedToCCR(rightSpeed)); }这里有一个实际使用中很值得注意的点不要从0直接给你目标速度。电机从静止突然到80%占空比电流冲击非常大L298N容易发热车也会猛冲。我通常在应用层做一个简单的加减速斜坡每10ms调整一步每次最多加减1%。这样车起步平稳对电池和模块都友好。3.4 封装正反转和停止函数有了上面这些底层函数上层逻辑就非常清爽了void Motor_Forward(uint8_t speed) { Motor_SetLeftDir(DIR_FORWARD); Motor_SetRightDir(DIR_FORWARD); Motor_SetSpeed(speed, speed); } void Motor_Backward(uint8_t speed) { Motor_SetLeftDir(DIR_BACKWARD); Motor_SetRightDir(DIR_BACKWARD); Motor_SetSpeed(speed, speed); } void Motor_Stop(void) { Motor_SetLeftDir(DIR_STOP); Motor_SetRightDir(DIR_STOP); Motor_SetSpeed(0, 0); }到这里一个能控制小车前进、后退、调速的驱动模块就完成了。接下来是这个项目的重头戏拐弯逻辑。4. 正反转与拐弯的控制逻辑差速转向的实现与按键控制既然标题里明确提到了“拐弯”那这部分必须单独展开。两轮驱动的车拐弯依赖的是左右轮的速度差。F103核心板配L298N这种双H桥驱动天生就是干这件事的关键是控制逻辑要写清楚。4.1 差速转向的三种形态我见过一些简化的小车代码把左转和右转直接写成“左轮转、右轮停”这种做法不是不行很生硬。实际工程里拐弯分三种形态弧线转弯左右轮同向但速度不同。转弯半径大适合高速跑动中微调方向。原地旋转左右轮反向一个正转一个反转。转弯半径为零适合在狭窄空间掉头。舵角转向左右轮速度相等只有前轮舵机打角度那是另一套结构不在本文讨论范围。用L298N实现前两种都容易。弧线转弯只需给两侧不同的速度原地旋转需要把一侧电机设为反转。这里有一个容易搞混的地方原地旋转时不能简单地分别调用Motor_Forward和Motor_Backward因为那样会让全局的方向状态冲突而且速度设置会被覆盖。我的做法是写两个专用的旋转函数void Motor_SpinLeft(uint8_t speed) { Motor_SetLeftDir(DIR_BACKWARD); Motor_SetRightDir(DIR_FORWARD); Motor_SetSpeed(speed, speed); } void Motor_SpinRight(uint8_t speed) { Motor_SetLeftDir(DIR_FORWARD); Motor_SetRightDir(DIR_BACKWARD); Motor_SetSpeed(speed, speed); } void Motor_TurnLeft(uint8_t speed) { // 弧线左转左轮减速到60%右轮保持speed Motor_SetLeftDir(DIR_FORWARD); Motor_SetRightDir(DIR_FORWARD); Motor_SetSpeed((uint8_t)(speed * 60 / 100), speed); } void Motor_TurnRight(uint8_t speed) { Motor_SetLeftDir(DIR_FORWARD); Motor_SetRightDir(DIR_FORWARD); Motor_SetSpeed(speed, (uint8_t)(speed * 60 / 100)); }弧线转弯时那个60%的比例可以根据实际车子的机械结构调。重心靠前、轮子大、摩擦大的车可能40%的差速就足够轮子小、重心靠后的车需要更大的速度差才能看出明显转弯。4.2 按键扫描程序怎么组织实际项目里你可能是用按键、蓝牙串口、遥控器接收机或者循迹传感器来触发这些动作。为了演示方便我这里给一个按键控制的框架按键引脚你可以根据自己核心板的原理图修改宏定义。// key.h #define KEY_UP_PIN GPIO_PIN_0 #define KEY_UP_PORT GPIOA #define KEY_DOWN_PIN GPIO_PIN_1 #define KEY_DOWN_PORT GPIOA按键扫描用10ms周期调用一次void Car_KeyControl(void) { static uint8_t speedNow 0; uint8_t speedTarget 50; // 默认目标速度 if (HAL_GPIO_ReadPin(KEY_UP_PORT, KEY_UP_PIN) GPIO_RESET) { // 按键松开检测防止一次按下触发多次 while (HAL_GPIO_ReadPin(KEY_UP_PORT, KEY_UP_PIN) GPIO_RESET); Motor_Forward(speedTarget); } else if (HAL_GPIO_ReadPin(KEY_DOWN_PORT, KEY_DOWN_PIN) GPIO_RESET) { while (HAL_GPIO_ReadPin(KEY_DOWN_PORT, KEY_DOWN_PIN) GPIO_RESET); Motor_Backward(speedTarget); } // 其余按键类似这里省略 }4.3 加速减速的斜坡处理前面提到了加减速斜坡这里给出一个完整可用的代码片段void Car_SetSpeedSmooth(uint8_t target) { static uint8_t current 0; if (current target) { current; } else if (current target) { current--; } Motor_SetSpeed(current, current); }这个函数放在10ms定时器中断里调用效果就是每秒速度最多变化100个单位电机起步非常平滑。如果想要更快的响应把步进改成2或者在5ms中断里调用。5. 实测中的五个大坑电源、共地、干扰、死区与校准最后这部分是我最想写的东西。代码和接线看起来都挺对但实际跑起来总会出各种幺蛾子。我把这几年做小车踩过的坑集中总结一下这些经验光看手册是学不到的。5.1 电源回路上的欠压复位与发热症状小车一加速单片机就重启蓝牙模块反复断开重连。用万用表量核心板的VCC发现电压从5V掉到3.2V左右。原因电机电源和逻辑电源共用一个电池电机启动瞬间电流很大电池内阻导致电压被拉低78M05的输入电压低于稳压最低值5V输出跟着往下掉。解决核心板独立供电或者至少给电机电源并联一个大容量电容。如果一定要共用电源选容量足够的电池比如18650两节串联而不是那种容量虚标的小电池。5.2 共地不良导致信号乱飞症状不共地时电机接通后L298N的IN1引脚不管单片机怎么拉电平都不稳定。接上共地线后一切正常。原因单片机输出的高电平是相对的它相对于自己的GND被识别为高L298N却以模块自己的GND为参考。两边基准不统一信号电平自然乱套。这个问题的排查很简单拿一根杜邦线把两个GND短接就验证了。如果你用的是无线遥控或者蓝牙控制还要确保接收端和驱动端共地良好否则会出现“按键没反应但接根线又好了”的怪现象。5.3 电机电刷干扰导致单片机死机症状小车在粗糙地面跑几秒后程序突然跑飞看门狗复位或者舵机乱抖。原因直流电机的电刷和换向器接触瞬间会产生火花这个火花是宽频带噪声能通过电源线传导到单片机。电机离单片机越近、电源线越长干扰越严重。解决电机两根电源线各串一个几uH的电感并在电机两端并一个104瓷片电容。单片机电源引脚旁边放一个100uF电解电容加一个104瓷片电容。如果还不行把整个单片机部分用铜箔胶带屏蔽或者用光耦隔离PWM信号后者属于高阶玩法一般小车用不上。5.4 PWM频率设置不当造成的异响和发热症状电机转起来发出尖锐的吱吱声L298N模块烫得不敢碰。原因PWM频率落在人耳听觉敏感范围2kHz~5kHz内听起来很刺耳同时频率太高导致L298内部开关损耗大发热严重。解决最省事的就是把频率调到1kHz以下缺点是电机低速时顿挫感稍强。如果你追求安静和平顺可以试试20kHz超过人耳范围听不到啸叫但L298发热会明显增加。折中方案是10kHz大多数减速电机在这个频率下声音和发热都还可以接受。5.5 左右轮速度不一致的软件校准症状小车看起来是直行实际跑起来总是往左偏。拆下轮子空转对比发现两个电机空载转速本身就有差异。原因电机并不是完全一致的工业品磁钢、绕组、电刷都有公差。两个电机即使占空比完全相同实际转速也可能差5%到10%。解决在驱动层加一个左右轮校准系数。比如左边电机快那就把左轮的CCR映射乘一个0.95的系数右边保持原样。实际做法是在代码里定义一个结构体typedef struct { int8_t leftTrim; // 左轮修正百分比-100~100 int8_t rightTrim; // 右轮修正百分比-100~100 } MotorTrim_t; static const MotorTrim_t g_motorTrim { -5, 0 }; // 左轮降5% static uint16_t SpeedToCCR(uint8_t speed, int8_t trim) { int16_t s speed trim; if (s 0) s 0; if (s MOTOR_SPEED_MAX) s MOTOR_SPEED_MAX; return (uint16_t)((uint32_t)s * PWM_ARR_VALUE / MOTOR_SPEED_MAX); }每次换上新车架、换电机之后都需要重新调这个修正系数。我一般在小车前方1米处贴一个十字标记手动调节校准值直到小车能直线跑到标记处不偏离。最后再分享一个我自己用下来的体会这种小车项目真正花时间的其实不是写代码而是接线和排查。硬件上没做好软件写得再漂亮也是空中楼阁。如果你第一次做这个项目建议按照我的步骤先只接一路电机验证PWM波形和方向控制再接第二路最后再考虑按键和转向。这样一旦出问题你很快就能定位是哪一部分的锅不至于对着几十行代码和一堆杜邦线发愁。本文还有配套的精品资源点击获取