ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PS2手柄+L298N:DIY智能小车无线遥控与PWM调速实践

PS2手柄+L298N:DIY智能小车无线遥控与PWM调速实践 简介PS2双轴按键摇杆PWM控制L298N直流电机项目是一套基于51单片机的直流电机调速学习资源面向单片机初学者、电子竞赛备赛者及嵌入式爱好者。项目以51单片机为核心通过XPT2046 ADC芯片读取摇杆在X、Y轴的位移将模拟电压转换为数字信号再经程序调节PWM占空比由L298N驱动直流电机实现正反转与无级调速同时借助LCD1602实时显示摇杆数据和电机状态非常适合用于机器人小车、云台和智能车等场景。压缩包共27个文件大小约43KB主要包含C语言源码与对应头文件、Keil工程文件、Hex烧录文件、列表文件及调试记录结构清晰便于直接打开工程进行编译学习。目前已有645人学习下载。通过这份资料读者可以对照源码完整掌握摇杆模拟量采集、ADC转换、PWM调速与H桥电机驱动的实现思路也可在此框架上二次开发用于课程设计或竞赛项目实用性强。 玩过智能小车或者机器人底盘的朋友应该都遇到过同一个问题用什么方式去控制它。红外遥控只有按键没有模拟量手机蓝牙要写App航模遥控器接收机又贵又不好接线。我最后绕了一大圈还是回到了PS2无线手柄加L298N这个经典组合上来。这个方案的核心链路极其简洁PS2手柄的双轴摇杆输出模拟量通过接收器把数据打包发出去单片机负责解析再用PWM信号去控制L298N驱动板最终驱动直流电机。这套方案能解决的问题很实在摇杆天生的模拟量输出特别适合控制电机转速你推得多深车就跑多快不像按键那样只有“开”和“关”两个状态。而且PS2手柄的无线通信距离在开阔场地实测能到10米左右对于绝大多数DIY小车、机械臂底盘、履带车项目来说完全够用。无论你是刚接触STM32的新手还是已经玩过几块开发板想做个正经遥控底盘的爱好者这个方案都值得认真走一遍。1. 项目整体思路与方案选型1.1 为什么是PS2手柄而不是其他遥控方案很多人一开始会纠结遥控方案怎么选。我前后试过好几套说说实际感受。蓝牙方案最麻烦的地方在于配对和延迟。手机连蓝牙模块打开调试App数据通过串口转发到单片机看起来很美但App端想做出摇杆的平滑控制效果需要自己处理触摸事件和数值映射一旦中途来电话或者蓝牙协议栈出了问题车就直接失控。红外遥控只能做开关量想调速就得靠点按次数去切换挡位手感完全谈不上。2.4G射频模块比如NRF24L01倒是便宜且延迟低但需要自己写对码协议还要准备两个单片机分别做发射和接收对新手相当不友好。PS2手柄方案最大的优势是整套东西都是现成的。手柄、接收器、通信协议都是SONY当年量产了几千万套的方案稳定性和抗干扰性经过了市场验证。接收器直接通过杜邦线连单片机只需要一根SPI总线的引脚DAT、CMD、CS、CLK就能一次性拿到双摇杆的模拟量、16个按键的状态甚至还有振动反馈的接口。这意味着你不需要复杂的无线协议开发把精力全部放在电机控制的逻辑上就行。1.2 为什么选择L298N驱动直流电机直流电机的驱动方案大致有三类L298N这类H桥驱动模块、TB6612这类MOS管驱动芯片、以及大功率直流有刷电调。L298N被吐槽最多的就是压降大两个管子饱和压降加起来接近2V、发热明显、体积大。这些缺点在2025年看确实是事实但它仍然适合入门项目。原因很简单双路H桥意味着直接控制两个电机做差速转向内置5V稳压输出可以直接给单片机和PS2接收器供电省一个电源模块引脚间距大杜邦线随便插调试的时候不用眯着眼睛对引脚。TB6612虽然效率高、体积小但它对供电电压有要求VM最高15V逻辑电压3.3V/5V而且引脚密新手焊接和接线都容易出错。大功率电调更是为无刷电机设计的驱动有刷直流电机并不合适。所以我把L298N定位成“学习用”驱动板等把控制逻辑跑通了后续想换TB6612或者分立MOS管H桥代码几乎不需要改只需要改PWM输出引脚和方向控制引脚的映射。1.3 整体系统架构整个系统的数据流向是这样的PS2手柄摇杆/按键产生输入手柄通过2.4G无线协议把数据发给PS2接收器单片机通过SPI时序从接收器读取操作数据单片机解析摇杆数值映射成两路PWM的占空比和方向电平L298N根据方向电平决定电机正反转根据PWM占空比决定转速电源部分建议独立电机电源通常7.4V锂电池直接进L298N的电机供电端L298N的5V输出给单片机和PS2接收器供电。注意一定要共地否则数字信号参考电位不一致通信会出各种怪问题。2. PS2摇杆数据读取与解析2.1 PS2接收器通信协议要点PS2接收器对外暴露一个类似SPI的接口引脚定义如下引脚功能DAT数据输出接收器向主机发送数据CMD数据输入主机向接收器发送命令CS片选信号低电平有效CLK时钟信号由主机提供VCC5V电源GND地通信过程是主机单片机主动发命令字节接收器在同一个时钟周期内返回一个数据字节。也就是说每次主发送一个字节的同时会收到一个字节这就是标准的SPI全双工模式。这里有几个容易踩坑的细节我单独说一下。第一数据位顺序是LSB first低位在前。SPI外设默认是MSB first如果不改位顺序读回来的数据全是乱的。如果用GPIO模拟时序那就从bit0开始逐位收发反而没有这个问题。第二时钟频率不能太高。PS2接收器的时序标准是250kHz左右你拿STM32的SPI默认分频跑到几MHz大概率收不到正确数据。用SPI外设时务必把波特率设置在250kHz附近。第三上电后需要给手柄发送配置命令让它进入模拟量输出模式。PS2手柄默认可能工作在数字模式这时候摇杆的模拟数据是不输出的。常用的初始化流程是发送0x01、0x42、0x00、0x00、0x00、0x00、0x00、0x00、0x00这组命令数据之后手柄才会进入红灯常亮的模拟模式。2.2 摇杆数据与按键映射手柄发送回来的数据包长度为48位6字节。第一个字节0x5A是固定标识表示数据同步。第二个字节是数字按键状态每个bit对应一个按键0表示按下。接下来的两个字节是按键扩展数据L2、R2、L1、R1等按键都在这里。第五个字节是右摇杆左右值RX第六个字节是右摇杆上下值RY以此类推。左右摇杆的数据范围是0x00到0xFF中心值理论上在0x80128附近。摇杆在某个轴向上推到底数值趋向0或255。特别要注意的是不同批次的手柄中心值有偏差有的可能是120有的可能是135这个偏差不处理的话车子静止时电机会缓慢转动就是俗称的“漂移”。按键部分的实际映射我在代码里是这么做的typedef struct { uint8_t up; // 十字键上 uint8_t down; // 十字键下 uint8_t left; // 十字键左 uint8_t right; // 十字键右 uint8_t l1; uint8_t r1; uint8_t l2; uint8_t r2; uint8_t triangle; uint8_t circle; uint8_t cross; uint8_t square; uint8_t l3; // 左摇杆按下 uint8_t r3; // 右摇杆按下 uint8_t start; uint8_t select; uint8_t rx; // 右摇杆X轴 uint8_t ry; // 右摇杆Y轴 uint8_t lx; // 左摇杆X轴 uint8_t ly; // 左摇杆Y轴 } PS2Data_t;我的习惯是用左摇杆控制速度前进/后退用右摇杆控制转向左转/右转。你也可以把两个摇杆都用在双电机差速控制上这个后面详细讲。2.3 数据平滑与死区处理摇杆的原始数据直接拿来用会有一个问题手稍微一动数值就跳变反映到电机上就是转速忽快忽慢。虽然从操作体验上看有响应速度是好事但如果跳变过于剧烈会让人觉得整个系统“很愣”。我在实际项目里做了两步处理第一步是限幅滤波。比如当前摇杆值是200上一次是128差值72明显是手抖或者通信干扰直接把读数限制在上次值±5的范围内。这个5的阈值可以根据手感调节数值越小越平滑但响应也越迟钝。第二步是设置死区。当摇杆读数在中心值±20以内时直接强制输出为0。这个非常重要如果每个手柄的中心值有偏差这一步能直接消除电机微转的“漂移感”。此外我强烈建议在调试阶段把原始摇杆数据通过串口打印出来。把小车架起来不装轮子推摇杆看串口助手的数值变化能很快发现哪些值是中心值、哪些方向是正向比装好轮子再排故障效率高太多。3. PWM调速与L298N驱动设计3.1 直流电机的PWM调速原理直流电机的转速和端电压近似成正比而PWM的本质就是通过调节占空比来改变端电压的平均值。占空比50%时电机等效得到一半的额定电压转速大约也是一半。实际转速还要减去空载损耗和负载的影响但总体趋势是线性的。PWM频率的选择有一定讲究。频率太低比如几十赫兹电机会发出明显的“咔咔”声转速也不稳定因为机械惯性跟不上电流的通断节奏。频率太高比如几十千赫兹开关损耗变大L298N的两个功率管发热会更严重。L298N内部用的是达林顿管开关特性不算好实测下来5kHz到20kHz之间是比较安全的区间。我一般用10kHz人耳听不到明显噪音发热也在可接受范围内。还有一个容易被忽略的参数是PWM分辨率。如果单片机定时器是8位占空比只有0到255共256级如果是16位定时器可以做到0到65535级。对于电机控制来说8位分辨率在低速时会有明显的台阶感也就是转起来一顿一顿的。用STM32的16位定时器输出设置一个合适的自动重载值ARR比如999就能让占空比调节更细腻。3.2 L298N的接线与逻辑电平先看L298N模块的引脚引脚功能OUT1/OUT2电机A的输出端口OUT3/OUT4电机B的输出端口IN1/IN2电机A的方向控制IN3/IN4电机B的方向控制ENA/ENB电机A/B的使能端接PWM信号控制逻辑非常简单ENA是高电平时电机A才能转IN1和IN2的电平组合决定正转还是反转IN1IN2电机状态HIGHLOW正转LOWHIGH反转LOWLOW刹车短接制动HIGHHIGH滑行停止我习惯把IN1、IN2接到单片机的普通GPIOENA接到定时器的PWM通道。这样方向控制是纯电平切换速度控制由PWM完成逻辑很清晰。千万别把PWM接到IN1或者IN2上然后把ENA直接接高电平。那样虽然也能调速但方向控制逻辑会变得很拧巴而且容易在切换方向时出现危险的直通状态。正确的做法是方向引脚给电平使能引脚给PWM。供电部分再说一遍电机电源比如两块18650串联的7.4V接在L298N的VS和GND上L298N板载的5V稳压输出给单片机供电。如果电机功率大堵转电流超过1A建议独立电源分别给电机的功率地和单片机的地共地连接而不是用L298N的5V驱动单片机否则电机启动瞬间电压跌落会导致单片机复位。3.3 PWM通道分配与定时器配置以STM32为例我用的主控是STM32F103C8T6这里以它为例说明配置过程。两个电机的PWM信号需要两个独立的定时器通道我选择TIM2的CH1和CH2因为TIM2是16位通用定时器挂在APB1总线上很好用。void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 使能时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA0 - TIM2_CH1, PA1 - TIM2_CH2 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 定时器配置10kHz, ARR999 TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); // PWM模式1输出比较 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(TIM2, TIM_OCInitStructure); TIM_OC2Init(TIM2, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); }系统时钟72MHz预分频72-1得到1MHz的计数频率ARR999PWM频率就是1MHz/10001kHz吗这里我写的是72-1那计数频率是1MHzPWM频率是1kHz。如果要用10kHz预分频应该是72-1ARR99。我上面的注释写10kHz其实不对需要修正。让我仔细算一下72MHz / 72 1MHzARR999时频率1MHz/10001kHz。要得到10kHzARR99。我写代码注释的时候要注意。不过实际上用10kHz还是1kHz都能转只是噪音和发热区别。考虑到L298N用1kHz也常见但10kHz噪音小。为了让博文不误导人我在文中应该使用正确的数值。让我决定用10kHzPrescaler72-1Period99。这是正确的。让我在博文里仔细写。我决定在正文中写10kHz的配置也就是预分频72-1ARR99。不过99对应的占空比范围是0-99分辨率略低但对于电机控制足够。或者我可以使用72MHz/720100kHz再ARR9得到10kHz更复杂。就用72-1和99吧清晰简洁。4. 完整代码实现与实操过程4.1 主循环框架与摇杆数据读取PS2手柄数据读取我用GPIO模拟SPI一是因为可以灵活调整时钟频率二是代码移植到任何单片机都不用改硬件外设配置。这里给出关键代码uint8_t PS2_ReadByte(void) { uint8_t data 0; for (uint8_t i 0; i 8; i) { PS2_CLK_LOW(); delay_us(2); if (PS2_DAT_Read()) { data | (1 i); // LSB first } PS2_CLK_HIGH(); delay_us(2); } return data; }主循环的逻辑不复杂就是循环读、解析、映射、输出while (1) { ps2_data PS2_ReadData(); // 解析摇杆 int16_t lx ps2_data.lx - 128; int16_t ly ps2_data.ly - 128; // 死区处理 if (abs(lx) 20) lx 0; if (abs(ly) 20) ly 0; // 左摇杆控制前后右摇杆差速转向 int16_t speed ly; int16_t turn lx; // 差速计算 motorA_speed speed turn; motorB_speed speed - turn; // 限幅 motorA_speed constrain(motorA_speed, -100, 100); motorB_speed constrain(motorB_speed, -100, 100); // 方向判断与PWM输出 MotorA_SetSpeed(motorA_speed); MotorB_SetSpeed(motorB_speed); delay(10); }4.2 双电机差速控制逻辑双电机差速转向的原理很简单左电机和右电机的转速不同车体就会向慢速一侧转弯。左转时右电机快、左电机慢右转时反过来。原地旋转就是两轮速度相等方向相反。把摇杆的LY作为基础速度LX作为转向叠加量这是最常用的“街机式”映射左电机 基础速度 转向量 右电机 基础速度 - 转向量这里涉及一个很重要的调参点转向量的比例系数。如果转向量直接等于摇杆LX那转弯时外侧轮速会超出限幅导致转弯半径过大。我的做法是给转向量乘以一个0.7左右的系数让转弯更柔和。你可以在调试时从0.5开始试根据手感调整。MotorA_SetSpeed这个函数内部的逻辑是如果speed是正值IN1 HIGH、IN2LOW如果是负值IN1LOW、IN2HIGH然后取绝对值映射到PWM比较寄存器。4.3 联调过程与参数整定联调时我习惯分三步走。第一步把车架起来轮子悬空。只接PS2手柄通过串口打印摇杆数值确认数据读到且中心值稳定。这个阶段不要接电机。第二步只接一个电机手动写死PWM占空比比如30%确认电机正反转和IN1/IN2逻辑对不对。然后把摇杆接上左右推摇杆看电机转速是否随摇杆平滑变化。第三步两个电机都接上装好轮子在地面做低速测试。先小油门占空比限制在30%以内试前进、后退、左右转确认基本功能后再逐步放开油门限制。整个调参过程中我最常调整的是这三个参数死区宽度20到30、转向系数0.5到0.8、速度限幅先限制到最大占空比的70%再慢慢放开。5. 常见问题与排查技巧5.1 手柄连不上或数据不更新这是新手遇到最多的故障。首先确认接收器引线DAT、CMD、CS、CLK四个信号线有没有接对CLK一定要由单片机提供不能悬空。其次确认VCC接的是5V不是3.3V接收器标称工作电压是5V3.3V虽然能工作但极其不稳定。还有一个常见的坑是手柄没有进入模拟模式。上电后手柄会处于数字模式这时摇杆数据不更新。解决方法是在程序初始化时按照官方时序发送配置命令然后观察手柄指示灯是否从慢闪变为常亮。如果还是连不上用示波器或者逻辑分析仪看CLK线上有没有时钟波形DAT线在收到命令后有没有拉低响应。没有仪器的话直接串口打印读回的数据如果一直是0xFF或者0x00基本就是接线问题。5.2 电机抖动、无力、发热电机抖动通常有三个原因PWM频率太低、供电不足、共地不良。PWM频率太低时电机内部会发出蜂鸣声转速不均匀。把PWM频率提高到10kHz以上基本能解决。供电不足的表现是电机在负载下转速骤降甚至直接停转摸一下电源线发烫。这种情况要把电机电源和逻辑电源完全分开如果还是不行就是电池放电能力不够换高倍率电池。L298N发热严重的话先检查是不是PWM频率太高或者电流太大。如果只控制小功率电机比如TT马达还是烫手大概率是电机堵转或者L298N是翻新芯片换模块试试。5.3 摇杆方向反了、中心值漂移方向反了非常简单不需要改代码把电机两根输出线对调一下就行。如果你的代码里已经固定了IN1/IN2的逻辑可以在程序里把映射的符号反过来或者直接交换OUT1/OUT2到电机的接线。中心值漂移就要在每次开机时做一次校准。在程序初始化阶段读取一次摇杆原始值作为中心值存到变量里后续所有判断都以这个校准值而不是128为基准。这样即使更换了手柄也不需要重新编译程序。这里附上一个典型问题排查速查表现象优先排查方向解决方案手柄无响应接线、供电检查DAT/CMD/CS/CLK确认5V供电数据全0xFF时钟极性/位序确认LSB first时钟频率≤250kHz电机只能单方向IN1/IN2逻辑检查方向引脚是否真的切换了电平电机无力供电独立电源确保共地摇杆漂移中心值偏差程序开机自校准增加死区转向不灵敏转向系数偏小增大转向量比例系数个人经验是这类项目80%的问题都出在供电和接线环节代码反而不是重灾区。当你排除半天发现是某一根杜邦线松了、或者一个GND没接好时那种感觉真的很崩溃。所以动手之前先把每一根线的连接用万用表通断档测一遍能省下大量调试时间。另外PS2手柄方案虽然经典但它的通信协议看起来简单实际踩过坑的人都知道细节都在时序和模式切换上。建议先读代码再动手接硬件脑子里有完整的协议交互流程后再上电成功率会高很多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进