☰
用赛车方向盘控制树莓派小车:evdev与PWM差速转向实战
2026/11/4 11:02:14 网站建设 项目流程

1. 项目缘起与整体设计思路

1.1 为什么想到用赛车模拟器方向盘来控制树莓派小车

手头有一台吃灰很久的树莓派小车底盘,四个直流电机加一个L298N驱动板,之前用手机热点加网页遥控的方式玩过一阵子,说实话体验很一般——手机触屏没有手感,转弯要么太猛要么太肉,速度控制全靠盲猜。后来收拾房间翻出来一套罗技G29赛车模拟器方向盘,踏板、排挡杆一应俱全,当时脑子里就冒出一个念头:这玩意儿本质上就是一堆电位器加按钮,树莓派能读GPIO,那能不能把方向盘接到树莓派上,用方向盘来控制小车?

这个想法其实并不复杂。赛车模拟器方向盘在PC上是通过USB HID协议被识别成游戏设备的,树莓派作为Linux主机同样可以识别它。关键在于怎么把方向盘的输入事件映射成小车的电机控制信号。我查了一圈资料,发现用Python的evdev库读取输入设备事件是最直接的路子,不需要写内核驱动,也不需要额外的单片机做中转。整个方案的核心思路就是:树莓派通过USB读取方向盘和踏板的输入事件,解析出转向角度、油门开度、刹车力度,再通过GPIO输出PWM信号控制L298N驱动板,最终驱动四个直流电机。

这个方案的优势在于:第一,方向盘和踏板本身就是为精细操控设计的,转向有900度行程,油门和刹车是线性霍尔传感器,控制精度远超手机触屏;第二,整套系统跑在树莓派上,不需要额外的电脑做中转,小车可以完全独立运行;第三,Python代码可读性好,后续想改映射逻辑或者加功能都很方便。适合有一定树莓派基础、想玩点不一样的东西的爱好者参考,小白也能跟着做,但需要提前补一下Linux输入子系统和PWM控制的基础知识。

1.2 硬件选型与连接方案

硬件清单其实很简单,核心就是树莓派、小车底盘、赛车方向盘三大部分。树莓派我用的是Raspberry Pi 4B 4GB版本,系统是Raspberry Pi OS Bullseye 64位。选4B的原因很简单:USB接口足够多,性能也够用,跑Python脚本毫无压力。如果你手头是树莓派3B+或者Zero 2 W,理论上也能跑,但Zero 2 W只有一个USB OTG口,需要加一个USB Hub才能同时接方向盘和调试用的键盘,稍微麻烦一点。

小车底盘是常见的四轮驱动亚克力底盘,配四个TT直流减速电机和L298N双H桥驱动模块。L298N的好处是便宜、皮实、控制逻辑简单,每个电机需要三根线:使能端(ENA/ENB)接PWM,方向控制端(IN1/IN2、IN3/IN4)接普通GPIO。这里要注意,L298N的使能端跳线帽一定要拔掉,否则电机只能全速转,没法调速。

赛车方向盘我用的是罗技G29,USB接口直接插到树莓派上。G29在Linux下会被识别成两个设备:一个是方向盘本体,一个是踏板。用lsusb命令可以看到罗技的厂商ID是046d。方向盘上的按钮、转向轴、油门、刹车、离合器都会以事件的形式上报,通过evdev库可以逐个读取。

供电方面需要特别注意:树莓派用5V 3A的电源单独供电,L298N和电机用7.4V锂电池供电,两者共地但不共电源。我试过用同一个电源给树莓派和电机供电,结果电机一启动树莓派就重启,原因是电机启动瞬间电流太大导致电压跌落。分开供电之后问题消失。这个坑后面还会详细说。

1.3 软件架构与数据流设计

软件层面我设计了一个三层结构:输入层、映射层、输出层。输入层负责从/dev/input/eventX读取方向盘和踏板的事件,解析出转向角度、油门值、刹车值、按钮状态。映射层把输入值转换成小车的运动指令,比如转向角度映射成左右轮差速,油门映射成前进PWM占空比,刹车映射成减速或反转。输出层通过RPi.GPIO库输出PWM信号和方向电平。

数据流是这样的:方向盘转动产生EV_ABS事件,evdev读取到ABS_X轴的数值变化,范围是0到65535,中间值大约32767。我把这个值归一化到-1.0到1.0之间,负值代表左转,正值代表右转。油门踏板对应ABS_Y轴,刹车对应ABS_Z轴,数值范围也是0到65535,归一化到0.0到1.0。然后根据转向值和油门值计算左右轮的PWM占空比:左轮占空比等于油门值乘以(1加上转向值乘以转向系数),右轮占空比等于油门值乘以(1减去转向值乘以转向系数)。转向系数我设的是0.8,这样满舵时内侧轮会减速甚至反转,实现原地转向的效果。

这个架构的好处是解耦彻底,输入层和输出层互不关心对方怎么实现,映射层可以随便改。比如你想把方向盘换成手柄,只需要改输入层;想把直流电机换成步进电机,只需要改输出层。后续想加陀螺仪做闭环控制,也只需要在映射层里插入一个PID控制器就行。

2. 核心细节解析与实操要点

2.1 树莓派识别赛车方向盘的完整过程

把G29插到树莓派USB口上之后,第一件事是确认系统有没有认出来。打开终端,输入lsusb,如果看到Logitech, Inc. G29 Driving Force Racing Wheel就说明USB层面已经识别了。但USB识别不代表输入子系统识别,还需要看/dev/input/目录下有没有新增的event设备。我一般用ls /dev/input/对比插拔前后的变化,插上方向盘之后多出来的event节点就是它。

更准确的方法是安装evtest工具,运行sudo evtest,它会列出所有输入设备让你选择。选中方向盘对应的设备后,转动方向盘、踩踏板,终端会实时打印出事件类型、代码和数值。这一步非常关键,因为不同型号的方向盘上报的轴代码可能不一样。比如G29的转向轴是ABS_X,油门是ABS_Y,刹车是ABS_Z,但有些方向盘可能用ABS_RX或者别的代码。你必须先用evtest确认清楚,否则后面代码里读错了轴,方向盘转了没反应,排查起来很浪费时间。

还有一个细节:G29在树莓派上电时如果方向盘没有回正,系统会把当前位置当作零点。所以每次启动程序之前,最好把方向盘转到中间位置,或者代码里做一次自动校准——启动后前两秒读取转向轴的最大值和最小值,取中间值作为零点。我两种方法都试过,自动校准更省心,但要求启动时方向盘不要被手按住。

2.2 用evdev库读取输入事件的代码实现

evdev是Python里读取Linux输入设备事件的标准库,安装很简单:pip install evdev。核心代码就几行:

from evdev import InputDevice, categorize, ecodes wheel = InputDevice('/dev/input/event3') pedals = InputDevice('/dev/input/event4') for event in wheel.read_loop(): if event.type == ecodes.EV_ABS: if event.code == ecodes.ABS_X: steering = event.value

但实际用的时候有几个坑。第一,read_loop()是阻塞的,如果你同时要读方向盘和踏板两个设备,需要开两个线程分别读,或者用select多路复用。我一开始图省事开了两个线程,结果发现Python的GIL导致两个线程抢锁,事件延迟很明显。后来改用asyncio加evdev的异步接口,流畅多了。

第二,event.value的范围不是固定的。G29的转向轴范围是0到65535,但有些方向盘是-32767到32767。代码里不能写死,要用device.absinfo(ecodes.ABS_X)获取min和max,然后动态归一化。这个细节很多教程里没提,但不做的话换一个方向盘就得改代码。

第三,踏板设备有时候会上报EV_KEY事件而不是EV_ABS,尤其是便宜的方向盘踏板用的是开关而不是霍尔传感器。这种情况下油门只有0和1两个值,没法做线性控制。买方向盘之前最好确认一下踏板类型,霍尔传感器的体验好很多。

2.3 L298N驱动板与PWM调速的关键参数

L298N的使能端接树莓派的GPIO12和GPIO13,这两个引脚支持硬件PWM,频率可以设到1kHz以上。PWM频率的选择有讲究:频率太低电机会有啸叫声,频率太高L298N会发热。我试过100Hz、500Hz、1kHz、5kHz,最后定在1kHz,电机声音比较小,驱动板温度也正常。

占空比的设置范围是0到100。但实际用的时候我发现,占空比低于30的时候电机根本转不动,只会发出嗡嗡声。这是因为TT电机的启动扭矩不够,需要克服静摩擦力。所以代码里我做了一个死区处理:如果计算出来的占空比绝对值小于30,就置零;如果大于30,就线性映射到30到100之间。这样低速操控会舒服很多,不会出现方向盘微动但小车不动的情况。

方向控制端IN1到IN4接普通GPIO,输出高电平或低电平决定电机正反转。这里要注意L298N的逻辑真值表:IN1高电平、IN2低电平电机正转,IN1低电平、IN2高电平电机反转,两个都是低电平电机停止,两个都是高电平电机制动。我一般用停止而不是制动,因为制动会让电机瞬间短路,电流冲击比较大。

还有一个容易忽略的点:树莓派GPIO的驱动能力有限,直接驱动L298N的输入引脚没问题,但如果你用的是光耦隔离或者需要长距离接线,最好加一个74HC245缓冲芯片。我一开始没加,线长超过20厘米之后电机偶尔会抽风,加了缓冲芯片就稳定了。

3. 实操过程与核心环节实现

3.1 系统环境搭建与依赖安装

树莓派系统烧录好之后,第一件事是更新软件源和安装必要的工具。我习惯用SSH远程操作,所以先在raspi-config里打开SSH和I2C(虽然这个项目用不到I2C,但后面想加OLED显示屏的话会用到)。然后执行:

sudo apt update sudo apt upgrade -y sudo apt install -y python3-pip python3-dev evtest pip3 install evdev RPi.GPIO

这里有个小坑:Raspberry Pi OS Bullseye自带的Python是3.9,RPi.GPIO库在3.9上工作正常,但如果你升级到Bookworm,Python版本变成3.11,RPi.GPIO可能会报错。解决办法是用gpiozero库替代,或者手动编译安装RPi.GPIO的最新版本。我为了省事,一直用Bullseye没升级。

安装完依赖之后,把用户加入input组,否则读取/dev/input/eventX会提示权限不足:

sudo usermod -a -G input pi

然后重新登录使组权限生效。这一步很多教程里没写,但不做的话代码跑起来会报PermissionError,新手很容易卡在这里。

3.2 方向盘输入读取与归一化处理

完整的输入读取代码我封装成了一个类,初始化的时候自动扫描/dev/input/目录,找到罗技厂商ID对应的设备。核心逻辑是:

import evdev from evdev import InputDevice, ecodes def find_logitech_devices(): devices = [InputDevice(path) for path in evdev.list_devices()] return [d for d in devices if 'Logitech' in d.name] def normalize(value, min_val, max_val): return (value - min_val) / (max_val - min_val) * 2 - 1

归一化的时候要注意,转向轴需要映射到-1到1,油门和刹车映射到0到1。所以转向轴用上面的公式,油门和刹车用(value - min_val) / (max_val - min_val)。我一开始把油门也映射到-1到1,结果松油门的时候得到-1,小车直接倒车,吓了一跳。

读取事件的时候,我用了一个while True循环加try/except捕获OSError,因为方向盘拔掉的时候read_loop()会抛异常。捕获之后重新扫描设备,实现热插拔。这个功能在实际调试的时候很有用,不用每次插拔方向盘都重启程序。

3.3 电机控制逻辑与差速转向算法

差速转向的核心公式前面提过,这里展开说一下参数怎么调。假设油门值throttle范围0到1,转向值steering范围-1到1,转向系数k初始设为0.8:

left_speed = throttle * (1 + steering * k) right_speed = throttle * (1 - steering * k)

当steering为0时,左右轮速度相同,小车直行。当steering为1时,左轮速度是throttle * 1.8,右轮速度是throttle * 0.2,右轮慢左轮快,小车右转。当steering为-1时反过来。如果k设得太大,比如1.5,满舵时内侧轮速度会变成负值,也就是反转,实现原地转向。但TT电机反转响应比较慢,实际效果不如预期,所以我最后把k定在0.8,内侧轮最多减速到20%速度,转弯半径已经很小了。

PWM输出我用的是RPi.GPIO的PWM类,频率1kHz。每个电机对应一个PWM实例和一个方向控制引脚对。代码里封装了一个Motor类,初始化时传入PWM引脚和方向引脚,提供set_speed(speed)方法,speed范围-100到100,负值代表反转。内部自动处理死区和方向切换。

这里有个细节:切换电机方向的时候,最好先让电机停止一小段时间再反向,否则L298N的H桥可能会因为反向电流冲击而触发保护。我加了一个10毫秒的延时,实测下来电机寿命长很多。

3.4 整体联调与实测效果

第一次联调的时候,我把小车架起来让轮子悬空,然后转动方向盘、踩油门,观察轮子转速变化。结果发现方向盘转到底的时候,内侧轮确实减速了,但外侧轮速度没有明显提升,转弯效果不明显。排查后发现是PWM占空比上限设成了100,但throttle * 1.8算出来超过100之后被截断了。后来我把上限改成min(100, throttle * (1 + steering * k)),同时把基础油门值调低,留出足够的余量给差速,转弯就灵敏多了。

实测下来,这套系统在木地板上跑得很稳。方向盘900度行程对应小车从直行到最小转弯半径,操控精度比手机触屏高了一个数量级。油门踏板踩下三分之一左右小车开始缓慢移动,踩到底全速前进,刹车踏板踩下后小车迅速减速停止。排挡杆我暂时没用上,后面打算用来切换驾驶模式,比如前进挡、倒挡、空挡。

续航方面,7.4V 2000mAh锂电池满电可以跑大约40分钟,树莓派用5V 3A充电宝供电,能撑2小时以上。整体重量增加不多,小车灵活性没有明显下降。

4. 常见问题与排查技巧实录

4.1 方向盘识别不到或事件读取失败

这是最常见的问题,表现是运行程序后方向盘转了没反应,或者直接报错找不到设备。排查步骤我整理了一个速查表:

现象可能原因解决方法
lsusb看不到罗技设备USB线接触不良或供电不足换一根USB线,插到树莓派原生USB口而不是Hub上
lsusb能看到但/dev/input/没有新设备内核模块未加载运行sudo modprobe usbhid,或重启树莓派
evtest能看到事件但Python读不到权限不足把用户加入input组,或直接用sudo运行
事件数值一直是0轴代码选错了用evtest确认正确的ABS_代码
方向盘转到底数值不变轴范围没归一化用absinfo获取min/max动态归一化

还有一个特殊情况:有些方向盘在树莓派上会被识别成两个设备,一个是方向盘一个是踏板,但两个设备的event编号可能会变。我建议在代码里用设备名称而不是event编号来查找,这样插拔顺序变了也不影响。

4.2 电机不转或转速异常

电机不转的原因很多,我按排查顺序列一下。首先用万用表量L298N的输出端有没有电压,如果没有,检查使能端跳线帽有没有拔掉、PWM信号有没有输出。如果使能端电压正常但电机不转,检查方向控制端的电平组合是否正确。如果方向电平也对但电机还是不转,可能是电机线接错了,把电机线对调一下试试。

转速异常通常表现为电机忽快忽慢或者有异响。最常见的原因是PWM频率和电机不匹配,可以试着把频率从1kHz调到500Hz或者2kHz。另一个原因是电源功率不够,电机启动瞬间电压跌落导致树莓派复位。解决办法是电机和树莓派分开供电,或者在大电容并联在L298N的电源输入端,我并了一个1000uF的电解电容,效果很明显。

还有一种情况是电机转但小车不走,这通常是轮子打滑或者齿轮箱卡住了。检查轮子有没有拧紧,齿轮箱里有没有异物。TT电机的齿轮箱是塑料的,摔过之后容易卡齿,只能换电机。

4.3 操控手感调优与延迟优化

操控手感这个东西很主观,但有几个参数可以调。第一个是转向死区,方向盘中间大概有5度左右的虚位,如果不做死区处理,直行的时候小车会左右画龙。我在代码里加了死区判断:转向值绝对值小于0.05就当作0处理。第二个是油门响应曲线,线性映射踩多少给多少,但实际用的时候低速段太灵敏高速段太迟钝。我改成平方映射,throttle_out = throttle_in ** 2,低速段更细腻,高速段更暴力,操控感好很多。

延迟方面,从转动方向盘到电机响应,我实测大概有80到120毫秒的延迟。主要来源是Python的事件循环和PWM输出。优化方法有几个:把read_loop()换成asyncio异步读取,减少线程切换开销;把PWM频率提高到2kHz,电机响应更快;把代码里的print调试语句全部删掉,IO操作很耗时。优化之后延迟降到50毫秒左右,基本上感觉不到。

还有一个容易被忽略的点:树莓派的CPU频率调度器默认是ondemand,负载低的时候会降频,导致事件处理延迟忽高忽低。我把调度器改成performance模式,CPU一直跑在最高频率,延迟稳定多了。命令是echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor。

4.4 电源管理与电磁干扰处理

电源问题我踩过最大的坑就是电机和树莓派共电源导致树莓派反复重启。后来分开供电解决了,但新的问题又来了:电机启动的时候,方向盘会短暂失去连接,evdev报OSError。这是电磁干扰导致的,电机换向时产生的尖峰脉冲通过电源线串到了USB口。

解决办法有三个:第一,在电机两端各并联一个0.1uF的陶瓷电容,吸收换向火花;第二,在L298N的电源输入端并联一个1000uF电解电容和一个0.1uF陶瓷电容,滤除低频和高频干扰;第三,USB线用带磁环的屏蔽线。三个措施一起上之后,方向盘再也没掉过线。

电池方面,7.4V锂电池用久了电压会下降,降到6V左右的时候L298N的输出会不稳定,电机转速明显变慢。我加了一个低压报警功能,用树莓派的ADC(需要额外加ADS1115模块)监测电池电压,低于6.5V的时候让蜂鸣器响一声提醒换电池。如果你不想加ADC,也可以简单点,跑40分钟就换电池,别等到跑不动了再换。

5. 后续扩展思路与个人经验分享

5.1 加装OLED显示屏实时显示状态

手头正好有一块0.96寸的OLED显示屏,I2C接口的,我打算把它装到小车上,实时显示电池电压、当前速度、转向角度这些信息。树莓派用luma.oled库驱动OLED很简单,接线就是VCC接3.3V、GND接GND、SDA接GPIO2、SCL接GPIO3。代码里开一个线程每秒钟刷新一次显示内容,不影响主控制循环。

OLED显示的内容我规划了四行:第一行显示电池电压,第二行显示油门和刹车值,第三行显示左右轮PWM占空比,第四行显示当前驾驶模式。这样调试的时候不用连电脑看终端,直接看小车上的屏幕就行,方便很多。如果你也想加这个功能,注意OLED的I2C地址通常是0x3C,用i2cdetect -y 1可以确认。

5.2 用排挡杆切换驾驶模式

G29的排挡杆有六个档位加一个倒挡,每个档位对应一个按钮事件。我打算把一档到六档映射成不同的速度限制,比如一档限速30%、二档限速50%、三档限速70%、四档限速90%、五档和六档不限速。倒挡按下之后,油门踏板控制倒车,方向盘转向逻辑不变。空挡按下之后电机停止,相当于手刹。

这个功能的实现很简单,在输入层监听排挡杆的按钮事件,改变一个全局变量gear,映射层根据gear的值调整油门上限。代码改动量不大,但驾驶体验会丰富很多,尤其是低速精细操控的时候,限速功能很实用。

5.3 个人实操心得与避坑建议

最后分享几个我在这个项目里踩过的坑和总结的经验。第一,不要用树莓派的5V引脚给L298N的逻辑电路供电,虽然理论上可以,但电机干扰会通过电源线串到树莓派,导致各种莫名其妙的问题。L298N的逻辑电源单独用一块小锂电池或者从电机电池降压取电,隔离效果最好。

第二,evdev读取事件的时候,read_loop()返回的事件对象里event.value是原始值,但event.code和event.type需要和ecodes里的常量对比。我一开始把event.code和event.type搞反了,排查了半天才发现。记住:event.type是事件大类(EV_ABS、EV_KEY),event.code是具体轴或按钮(ABS_X、BTN_A)。

第三,调试的时候一定要把小车架起来让轮子悬空,否则小车突然窜出去撞到东西或者掉下桌子,轻则摔坏底盘,重则烧掉电机驱动。我一开始没架起来,结果小车从桌上冲下去,L298N的散热片摔掉了,还好芯片没坏。

第四,Python的RPi.GPIO库在程序退出的时候不会自动清理GPIO状态,电机可能会保持最后一次的PWM输出继续转。所以代码里一定要用try/finally确保退出时调用GPIO.cleanup(),把所有引脚置低电平。我试过一次程序崩了没清理,小车一直往前跑,追了半天才抓住。

这个项目从起意到跑通大概花了两个周末的时间,中间大部分时间都花在排查电源干扰和调参上。但跑通之后那种用方向盘操控小车的感觉确实很爽,尤其是转弯时内侧轮减速的差速效果,比手机遥控有意思多了。如果你手头也有吃灰的方向盘和树莓派,强烈建议试试,成本不高,乐趣不少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询