树莓派智能小车项目:从硬件搭建到Python编程的嵌入式开发实践
1. 项目概述与核心价值
如果你对硬件编程和机器人制作感兴趣,但又被复杂的电路和底层代码吓退,那么这个基于树莓派的智能小车项目,可能就是为你量身定制的“第一块敲门砖”。它不像那些动辄需要焊接数百个元件的复杂机器人,更像是一个精心设计的“乐高”套装:用树莓派作为大脑,用Python作为指挥棒,通过简单的接线和清晰的代码,让一堆零散的硬件“活”起来,变成一个能听你指令前进、后退、转弯的智能小车。这个项目的核心价值在于,它完整地串联了从硬件认知、电路搭建到软件控制的全链路,让你在动手实践中,直观地理解嵌入式系统开发中“软件驱动硬件”这一核心理念。无论是电子爱好者、编程初学者,还是想为教学寻找生动案例的教师,都能从中获得清晰的路径和可复现的成果。整个过程,你将亲身体验如何将一行行Python代码,转化为电机转动的物理动作,这种“所见即所得”的成就感,是单纯学习理论无法比拟的。
2. 硬件选型与物料清单解析
动手之前,理清每一件物料的作用和选型理由至关重要。这不仅能帮你一次性备齐材料,更能让你在组装时心中有数,知道每个部件“为什么在这里”。
2.1 核心控制器:树莓派的选择
树莓派是这个项目的“大脑”,负责运行操作系统、解释Python程序并发出控制指令。原教程推荐树莓派3,这是一个非常稳妥的选择。其核心优势在于拥有4个USB端口和完整的40针GPIO排针,为连接电机驱动板、键盘鼠标等外设提供了充足接口。更重要的是,树莓派3的性能足以流畅运行Raspberry Pi OS(原Raspbian)并处理我们的电机控制逻辑,同时社区支持广泛,遇到问题容易找到解决方案。
注意:如果你手头有更新的树莓派4或树莓派5,完全可以替代,性能更强,但需注意树莓派4/5的供电接口是USB-C,而树莓派3是Micro USB,准备电源适配器时需对应。对于本项目,任何一款拥有40针GPIO的树莓派(Zero W除外,因其GPIO需焊接排针)均适用。
2.2 动力与驱动系统
这是让小车动起来的核心模块,包括电机、驱动板和电源。
直流减速电机(DC Gear Motor):我们选择两个直流减速电机,而非普通直流电机。关键在“减速”二字。普通直流电机转速高、扭矩小,直接驱动轮子会导致小车速度过快、力量不足,容易打滑且难以控制。减速电机内部集成了齿轮箱,牺牲了一定转速,但大幅提升了输出扭矩,使得小车起步、爬坡(过门槛、地毯)更有力。通常,我们选择工作电压在3-6V,带有减速箱的TT电机,它们价格低廉且配套轮子容易购买。
双路直流电机驱动板:树莓派的GPIO引脚只能提供很小的电流(约16mA),根本无法直接驱动电机。因此,我们需要一个电机驱动板作为“功率放大器”和“指挥官”。双路驱动板意味着它可以独立控制两个电机。常见的芯片有L298N或TB6612FNG。这里强烈推荐使用TB6612FNG芯片的驱动板。相比L298N,TB6612FNG效率更高、发热量小,且无需像L298N那样额外配置散热片,电路更简洁,特别适合电池供电的移动平台。
电源方案:这里需要理解一个关键概念:树莓派和电机必须分开供电。树莓派需要稳定、干净的5V电压(约2.5A电流),而电机启动和运行时会产生较大的电流波动和电气噪声,如果共用电源,极易导致树莓派重启或损坏。因此,我们采用双电源方案:
- 树莓派电源:一个标准的5V/2.5A以上的USB电源适配器。
- 电机电源:一个4节AA(5号)电池盒,可提供约6V电压(4*1.5V),直接为电机驱动板供电。电池盒的电压正适合我们选用的3-6V电机。
2.3 车体与辅助材料
- 车体(底盘):一个小纸盒或塑料盒作为底盘,简单易得。但如果你希望小车更坚固、外观更专业,可以购买亚克力或玻纤材质的智能小车底盘套件,上面通常已经预留了电机和树莓派的安装孔。
- 轮子:两个与电机轴配套的轮子。确保轮子的孔径与电机轴的直径(常见为3mm或6mm)匹配。
- 万向轮或滚珠轴承:作为小车的第三或第四个支撑点,保证其平稳运行。这是非必需但强烈推荐的部件。没有它,小车相当于一个“两轮自行车”,极难保持平衡。一个简单的球形万向轮安装在底盘前部或后部中央,成本极低,但能极大提升小车运动稳定性。
- 连接线:需要大量公对公、公对母的杜邦线,用于连接树莓派GPIO与电机驱动板,以及驱动板与电机、电池盒。
- 工具:螺丝刀(十字/一字)、剥线钳、电工胶带。焊接(电烙铁、焊锡丝)是可选项,但用焊接方式连接电机和导线,其可靠性和耐用性远高于直接插接,特别是对于长期运行或后续升级的项目。
3. 系统搭建与软件环境配置
在组装硬件之前,我们先为树莓派这颗“大脑”安装好操作系统和必要的编程环境。这个过程就像给新电脑装系统一样,是后续所有工作的基础。
3.1 烧录树莓派操作系统
首先,你需要一张至少8GB的Micro SD卡。我们将使用官方的“Raspberry Pi Imager”工具来烧录系统,这是目前最安全、最便捷的方法。
- 下载烧录工具:在任何一台Windows、macOS或Linux电脑上,访问树莓派官网,下载“Raspberry Pi Imager”并安装。
- 选择操作系统:打开Imager,点击“Choose OS”。对于本项目,选择第一项“Raspberry Pi OS (32-bit)”。这是一个基于Debian的、带有图形化桌面的操作系统,对新手非常友好。
- 选择存储设备:点击“Choose Storage”,选中你插入电脑的Micro SD卡读卡器对应的盘符,操作前请务必确认,避免误格式化其他磁盘。
- 进行高级设置(关键步骤):在点击“WRITE”之前,先按下键盘上的
Ctrl+Shift+X组合键,打开高级设置菜单。这里有几个必选项:- 设置主机名:例如
raspberrypi-buggy,方便在网络中识别。 - 启用SSH:勾选“Enable SSH”,并选择“Use password authentication”。这允许你后期通过其他电脑远程登录树莓派,无需连接屏幕和键盘,非常方便。
- 设置用户名和密码:务必设置!默认用户
pi已被废弃,你需要创建一个新用户名(如buggyuser)和密码。 - 配置无线网络:输入你的Wi-Fi名称(SSID)和密码,这样树莓派启动后就能自动联网。
- 设置区域选项:将时区(Timezone)设置为
Asia/Shanghai。
- 设置主机名:例如
- 烧录:设置完成后,点击“WRITE”,工具会自动下载系统镜像并烧录到SD卡中,同时应用你的高级设置。等待进度条完成。
3.2 首次启动与基础配置
将烧录好的SD卡插入树莓派的卡槽,连接HDMI线到显示器,插上USB键盘鼠标,最后连接5V电源适配器上电。
- 系统初始化:首次启动会进行一些初始化设置,等待片刻即可进入图形化桌面。
- 终端更新:点击顶部菜单栏的终端图标(黑色电脑屏幕图标),打开命令行窗口。首先更新软件源列表和升级所有已安装的软件包,这是一个好习惯,能确保系统安全和软件最新。
输入命令后可能需要输入你的用户密码。升级过程视网络情况可能需要10-30分钟。sudo apt update sudo apt full-upgrade -y - 安装Python库:树莓派OS已预装Python3。我们需要安装用于控制GPIO的库。最常用的是
gpiozero和RPi.GPIO。gpiozero是树莓派官方推荐的、更高层、更易用的库。sudo apt install python3-gpiozero python3-pigpio -ypigpio是一个守护进程,能提供更精确的硬件定时控制,对于电机PWM调速有益。
3.3 安装集成开发环境
你需要一个写代码的地方。虽然系统自带Thonny IDE,但对于本项目,使用MU编辑器或VS Code都是不错的选择。
方案一:安装MU编辑器(原教程选择)MU是一款为初学者设计的轻量级Python编辑器,内置了与GPIO交互的特殊模式。
sudo apt install mu-editor -y安装后,可以在“编程”菜单中找到它。
方案二:安装VS Code(个人推荐)VS Code功能更强大,支持代码提示、调试、版本控制等,更适合长期学习和开发。
sudo apt install code -y安装后,你还需要安装Python扩展。打开VS Code,点击左侧活动栏的扩展图标,搜索“Python”,安装Microsoft官方发布的Python扩展。
实操心得:对于完全新手,MU的简洁和内置的GPIO、绘图工具确实友好。但如果你有一点点编程经验,或者希望这个开发环境能用于更复杂的未来项目,我强烈建议直接使用VS Code。它的自动补全和错误提示能极大提升编码效率,减少因拼写错误导致的调试时间。
4. 硬件电路连接与组装详解
这是将零散部件整合成一台可工作实体的关键步骤。接线务必仔细,错误的连接可能损坏设备。
4.1 电机与驱动板的连接
我们以常见的TB6612FNG双电机驱动模块为例,讲解接线逻辑。请务必对照你手中驱动板的引脚标识。
电机驱动板电源输入:
- 将4节AA电池盒的正极(红线)连接至驱动板的
VM或VCC引脚(电机电源正极)。 - 将电池盒的负极(黑线)连接至驱动板的
GND引脚。
- 将4节AA电池盒的正极(红线)连接至驱动板的
电机输出:
- 驱动板上有两组输出端子:
A+/A-和B+/B-,分别对应左电机和右电机。 - 将左电机的两根线分别接入
A+和A-。此时无需区分正负,如果后续发现电机转向与预期相反,只需将这两根线对调即可。 - 同理,将右电机接入
B+和B-。
- 驱动板上有两组输出端子:
驱动板逻辑电源与地:
- 驱动板本身也需要一个低电压来工作其内部逻辑电路。将树莓派的5V引脚(例如物理引脚2或4)连接到驱动板的
VCC或VDD(逻辑电源)。 - 将树莓派的任意一个GND引脚(例如物理引脚6、9、14等)连接到驱动板的
GND(逻辑地)。这一步至关重要,它让树莓派和驱动板拥有共同的参考地电位。
- 驱动板本身也需要一个低电压来工作其内部逻辑电路。将树莓派的5V引脚(例如物理引脚2或4)连接到驱动板的
4.2 树莓派GPIO与驱动板控制线的连接
这是实现程序控制的核心。TB6612FNG每个电机通道需要3个控制信号。我们定义左电机为Motor A,右电机为Motor B。
| 树莓派 GPIO 引脚 (BCM编号) | 连接至驱动板引脚 | 功能说明 |
|---|---|---|
| GPIO17 (物理引脚11) | AIN1 | 电机A方向控制位1 |
| GPIO18 (物理引脚12) | AIN2 | 电机A方向控制位2 |
| GPIO22 (物理引脚15) | PWMA | 电机A调速PWM信号 |
| GPIO23 (物理引脚16) | BIN1 | 电机B方向控制位1 |
| GPIO24 (物理引脚18) | BIN2 | 电机B方向控制位2 |
| GPIO25 (物理引脚22) | PWMB | 电机B调速PWM信号 |
接线原理解释:
AIN1/AIN2和BIN1/BIN2是方向控制引脚。通过给它们输入不同的高低电平组合(00, 01, 10, 11),可以控制电机停止、正转、反转。具体真值表需查阅你的驱动板手册。PWMA/PWMB是调速引脚。树莓派GPIO可以输出PWM(脉冲宽度调制)信号,通过改变脉冲的占空比(高电平时间占整个周期的比例),来模拟不同的电压,从而实现电机转速的调节。占空比0%代表停止,100%代表全速。
重要注意事项:
- 务必使用BCM编号:在Python代码中,我们使用GPIO的BCM编号(Broadcom编号),而不是物理引脚序号。上表已同时给出。
- 先接线,后上电:所有接线操作必须在树莓派和电池盒都断电的情况下进行。
- 检查短路:接线完成后,仔细检查是否有裸露的线头互相触碰,特别是电源正负极之间。
4.3 机械结构组装
- 固定电机:在底盘(小盒子)两侧对称位置,开出能让电机减速箱部分塞入的方孔或圆孔。使用热熔胶或强力双面胶将电机牢牢固定在底盘内侧。确保两个电机的轴心高度一致,且轴线平行。
- 安装轮子:将轮子直接按压到电机轴上。如果轴是光滑的,可以滴一滴胶水(如401胶水)加固,但注意不要流到电机轴承里。
- 安装万向轮:在底盘前端或后端的中央位置,用螺丝或胶水固定一个万向轮。它承担支撑和导向的作用。
- 安置电子设备:将树莓派、驱动板用尼龙柱或胶固定在底盘上。将电池盒也找个位置固定好。尽量让重心分布均匀,并确保线路不会被轮子或运动部件缠绕。
5. Python编程控制核心实现
硬件就绪后,我们通过Python代码赋予小车灵魂。我们将从最简单的运动控制开始,逐步增加复杂度。
5.1 基础运动函数封装
首先,我们创建一个Python文件,例如buggy_controller.py。我们将使用gpiozero库,它抽象了底层细节,让控制变得非常直观。
#!/usr/bin/env python3 """ 树莓派智能小车基础控制库 基于gpiozero和TB6612FNG驱动板 """ from gpiozero import PWMOutputDevice, DigitalOutputDevice from time import sleep class Motor: """控制一个直流电机的类""" def __init__(self, in1_pin, in2_pin, pwm_pin): """ 初始化电机控制引脚 :param in1_pin: 方向控制引脚1 (BCM编号) :param in2_pin: 方向控制引脚2 (BCM编号) :param pwm_pin: PWM调速引脚 (BCM编号) """ # 方向控制引脚,初始化为低电平 self.in1 = DigitalOutputDevice(in1_pin, initial_value=False) self.in2 = DigitalOutputDevice(in2_pin, initial_value=False) # PWM调速引脚,频率设为1000Hz,初始占空比0(停止) self.pwm = PWMOutputDevice(pwm_pin, frequency=1000, initial_value=0) def forward(self, speed=1.0): """电机正转 :param speed: 速度,范围 0.0 ~ 1.0 """ self.in1.on() self.in2.off() self.pwm.value = max(0.0, min(1.0, speed)) # 限制速度范围 def backward(self, speed=1.0): """电机反转""" self.in1.off() self.in2.on() self.pwm.value = max(0.0, min(1.0, speed)) def stop(self): """电机停止""" self.in1.off() self.in2.off() self.pwm.value = 0 def brake(self): """电机刹车(短接两端)""" self.in1.on() self.in2.on() self.pwm.value = 0 # PWM保持0 class Buggy: """小车控制主类""" def __init__(self): # 根据之前的接线定义引脚 (BCM编号) # 左电机 self.left_motor = Motor(in1_pin=17, in2_pin=18, pwm_pin=22) # 右电机 self.right_motor = Motor(in1_pin=23, in2_pin=24, pwm_pin=25) def move_forward(self, speed=0.5, duration=None): """前进""" self.left_motor.forward(speed) self.right_motor.forward(speed) if duration: sleep(duration) self.stop() def move_backward(self, speed=0.5, duration=None): """后退""" self.left_motor.backward(speed) self.right_motor.backward(speed) if duration: sleep(duration) self.stop() def turn_left(self, speed=0.5, duration=None): """原地左转(左轮后退,右轮前进)""" self.left_motor.backward(speed) self.right_motor.forward(speed) if duration: sleep(duration) self.stop() def turn_right(self, speed=0.5, duration=None): """原地右转""" self.left_motor.forward(speed) self.right_motor.backward(speed) if duration: sleep(duration) self.stop() def stop(self): """停止""" self.left_motor.stop() self.right_motor.stop() def cleanup(self): """清理GPIO资源,程序退出前调用""" self.stop() # gpiozero会自动清理,这里显式调用确保 self.left_motor.pwm.close() self.left_motor.in1.close() self.left_motor.in2.close() self.right_motor.pwm.close() self.right_motor.in1.close() self.right_motor.in2.close() # 测试代码 if __name__ == "__main__": car = Buggy() try: print("测试:前进2秒") car.move_forward(speed=0.6, duration=2.0) sleep(1) print("测试:右转1秒") car.turn_right(speed=0.5, duration=1.0) sleep(1) print("测试:后退2秒") car.move_backward(speed=0.6, duration=2.0) sleep(1) print("测试:左转1秒") car.turn_left(speed=0.5, duration=1.0) print("测试完成!") except KeyboardInterrupt: print("程序被用户中断") finally: car.cleanup() print("GPIO资源已清理")代码解析与技巧:
- 类封装:我们将电机和小车封装成类,使代码结构清晰,易于管理和扩展。
- 速度限制:在
Motor类的forward和backward方法中,使用max(0.0, min(1.0, speed))来确保速度值在0到1之间,避免非法值导致错误。 - 刹车功能:
brake方法通过同时将两个方向控制引脚置高,短接电机两端,产生一个制动力矩,让小车更快停下,比单纯切断电源(stop)更有效。 - 资源清理:在
cleanup方法中显式关闭所有GPIO设备,这是一个好习惯,确保程序退出后GPIO状态被正确重置。
5.2 实现键盘遥控功能
让小车动起来很有趣,但用代码预设动作还不够交互。接下来,我们实现一个通过键盘按键实时控制小车的程序。这需要用到pynput库来监听键盘事件。
首先安装pynput:
pip install pynput然后创建keyboard_control.py:
#!/usr/bin/env python3 """ 键盘遥控小车程序 使用WASD或方向键控制 """ from pynput import keyboard from buggy_controller import Buggy # 导入我们刚才写的小车类 import time class KeyboardController: def __init__(self): self.car = Buggy() self.current_speed = 0.6 self.turn_speed = 0.4 # 记录当前按键状态 self.key_state = { 'up': False, 'down': False, 'left': False, 'right': False } print("键盘遥控已启动!") print("控制方式:") print(" W / 上箭头键 : 前进") print(" S / 下箭头键 : 后退") print(" A / 左箭头键 : 左转") print(" D / 右箭头键 : 右转") print(" 空格键 : 停止") print(" Q / ESC : 退出程序") def update_movement(self): """根据当前按键状态更新小车运动""" # 先停止所有动作 self.car.stop() # 处理前进/后退(上下键) if self.key_state['up'] and not self.key_state['down']: # 前进 + 转向组合 if self.key_state['left']: self.car.left_motor.forward(self.current_speed * 0.7) # 左轮慢一些 self.car.right_motor.forward(self.current_speed) elif self.key_state['right']: self.car.left_motor.forward(self.current_speed) self.car.right_motor.forward(self.current_speed * 0.7) # 右轮慢一些 else: self.car.move_forward(self.current_speed) elif self.key_state['down'] and not self.key_state['up']: # 后退 + 转向组合 if self.key_state['left']: self.car.left_motor.backward(self.current_speed * 0.7) self.car.right_motor.backward(self.current_speed) elif self.key_state['right']: self.car.left_motor.backward(self.current_speed) self.car.right_motor.backward(self.current_speed * 0.7) else: self.car.move_backward(self.current_speed) else: # 没有上下键,只有左右键(原地转向) if self.key_state['left'] and not self.key_state['right']: self.car.turn_left(self.turn_speed) elif self.key_state['right'] and not self.key_state['left']: self.car.turn_right(self.turn_speed) def on_press(self, key): """按键按下事件处理""" try: if key.char in ['w', 'W']: self.key_state['up'] = True elif key.char in ['s', 'S']: self.key_state['down'] = True elif key.char in ['a', 'A']: self.key_state['left'] = True elif key.char in ['d', 'D']: self.key_state['right'] = True elif key.char in ['q', 'Q']: print("退出程序...") return False # 停止监听器 except AttributeError: # 处理特殊键 if key == keyboard.Key.up: self.key_state['up'] = True elif key == keyboard.Key.down: self.key_state['down'] = True elif key == keyboard.Key.left: self.key_state['left'] = True elif key == keyboard.Key.right: self.key_state['right'] = True elif key == keyboard.Key.space: self.key_state = {k: False for k in self.key_state} # 清空所有状态 self.car.stop() print("已停止") elif key == keyboard.Key.esc: print("退出程序...") return False # 停止监听器 self.update_movement() return True def on_release(self, key): """按键释放事件处理""" try: if key.char in ['w', 'W']: self.key_state['up'] = False elif key.char in ['s', 'S']: self.key_state['down'] = False elif key.char in ['a', 'A']: self.key_state['left'] = False elif key.char in ['d', 'D']: self.key_state['right'] = False except AttributeError: if key == keyboard.Key.up: self.key_state['up'] = False elif key == keyboard.Key.down: self.key_state['down'] = False elif key == keyboard.Key.left: self.key_state['left'] = False elif key == keyboard.Key.right: self.key_state['right'] = False self.update_movement() return True def run(self): """启动键盘监听""" # 使用非阻塞监听模式 listener = keyboard.Listener( on_press=self.on_press, on_release=self.on_release) listener.start() try: # 主循环,保持程序运行 while listener.running: time.sleep(0.1) except KeyboardInterrupt: print("程序被中断") finally: listener.stop() self.car.cleanup() print("程序结束,GPIO已清理") if __name__ == "__main__": controller = KeyboardController() controller.run()代码亮点与避坑指南:
- 组合键处理:
update_movement方法巧妙处理了组合按键(如同时按“上”和“左”),让小车可以边前进边转弯,实现更平滑的弧线运动,而不是僵硬的先直行再转向。 - 状态机思想:使用
key_state字典记录每个方向键的持续状态,而不是在按键事件中直接控制电机。这样处理更符合控制逻辑,避免了按键抖动带来的问题。 - 资源释放:在
finally块中确保键盘监听器停止和小车GPIO资源被清理,即使程序异常退出也能保证系统稳定。 - 非阻塞监听:
pynput的Listener默认在新线程中运行,不会阻塞主程序。这为我们后续扩展(例如加入传感器数据读取循环)留下了空间。
运行这个程序,你的小车就能通过键盘自由驰骋了。这已经是一个完整的、交互式的机器人原型。
6. 功能扩展与进阶思路
基础运动实现后,你可以以此为平台,添加各种传感器和功能,让小车变得更“智能”。
6.1 添加超声波模块实现避障
避障是机器人最基础的功能之一。我们可以添加一个HC-SR04超声波测距模块。
硬件连接:
VCC-> 树莓派 5VGND-> 树莓派 GNDTrig(触发) -> GPIO5 (物理引脚29)Echo(回响) -> GPIO6 (物理引脚31)
软件实现: 创建一个新的类或函数来读取距离。这里需要注意,HC-SR04的Echo引脚返回的是高电平脉冲,其宽度与距离成正比。我们可以使用gpiozero的DistanceSensor类,但它通常需要支持脉冲读写的引脚。一个更通用的方法是使用RPi.GPIO库进行微秒级计时。
import RPi.GPIO as GPIO import time class UltrasonicSensor: def __init__(self, trig_pin, echo_pin): self.TRIG = trig_pin self.ECHO = echo_pin GPIO.setmode(GPIO.BCM) GPIO.setup(self.TRIG, GPIO.OUT) GPIO.setup(self.ECHO, GPIO.IN) GPIO.output(self.TRIG, False) time.sleep(0.5) # 让传感器稳定 def get_distance(self): """获取距离,单位:厘米""" # 发送10us的高电平脉冲触发测距 GPIO.output(self.TRIG, True) time.sleep(0.00001) # 10微秒 GPIO.output(self.TRIG, False) # 等待Echo引脚变高,记录开始时间 while GPIO.input(self.ECHO) == 0: pulse_start = time.time() # 等待Echo引脚变低,记录结束时间 while GPIO.input(self.ECHO) == 1: pulse_end = time.time() # 计算脉冲持续时间 pulse_duration = pulse_end - pulse_start # 声音速度约343米/秒,除以2(因为声音是往返) distance = pulse_duration * 17150 distance = round(distance, 2) # 保留两位小数 # 有效距离通常在2cm-400cm if distance > 400 or distance < 2: return None # 超出有效范围 return distance def cleanup(self): GPIO.cleanup([self.TRIG, self.ECHO]) # 在主循环中集成避障逻辑 def autonomous_avoidance(car, sensor, safe_distance=20): """简单的自动避障循环""" try: while True: dist = sensor.get_distance() if dist is not None and dist < safe_distance: print(f"前方 {dist}cm 有障碍物,执行避障") car.stop() time.sleep(0.5) car.move_backward(0.5, duration=1.0) car.turn_right(0.6, duration=0.8) # 向右转一个角度 car.move_forward(0.6) # 继续前进 else: if dist: print(f"前方安全距离: {dist}cm") car.move_forward(0.5) time.sleep(0.1) # 每100ms检测一次 except KeyboardInterrupt: car.stop() sensor.cleanup()6.2 通过Web界面或手机APP遥控
让小车脱离键盘,通过Wi-Fi用手机或电脑浏览器控制,实用性大大提升。这需要创建一个简单的Web服务器。
可以使用轻量级的Flask框架来实现:
pip install flask创建一个web_control.py文件:
from flask import Flask, render_template_string, request, jsonify from buggy_controller import Buggy import threading import time app = Flask(__name__) car = Buggy() current_cmd = "stop" cmd_lock = threading.Lock() # 一个简单的HTML控制页面 HTML_PAGE = ''' <!DOCTYPE html> <html> <head> <title>树莓派小车遥控</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> body { text-align: center; font-family: Arial; padding: 20px; } .control-panel { margin: 30px auto; width: 300px; } .btn { width: 80px; height: 80px; margin: 5px; font-size: 24px; border: none; border-radius: 10px; background: #4CAF50; color: white; cursor: pointer; } .btn:active { background: #45a049; } #btnStop { background: #f44336; width: 260px; } #btnStop:active { background: #d32f2f; } .status { margin-top: 20px; padding: 10px; background: #f1f1f1; } </style> </head> <body> <h1>树莓派智能小车遥控器</h1> <div class="control-panel"> <div><button class="btn" id="btnForward">↑</button></div> <div> <button class="btn" id="btnLeft">←</button> <button class="btn" id="btnRight">→</button> </div> <div><button class="btn" id="btnBackward">↓</button></div> <div><button class="btn" id="btnStop">停 止</button></div> </div> <div class="status"> <p>状态: <span id="statusText">已停止</span></p> <p>IP: <span id="ipAddr">{{ ip }}</span></p> </div> <script> const buttons = ['Forward', 'Backward', 'Left', 'Right', 'Stop']; let currentStatus = 'stop'; function sendCommand(cmd) { fetch('/cmd/' + cmd, { method: 'POST' }) .then(response => response.json()) .then(data => { document.getElementById('statusText').textContent = '执行: ' + data.command; currentStatus = data.command; }); } // 为每个按钮绑定事件 buttons.forEach(btn => { document.getElementById('btn' + btn).addEventListener('mousedown', () => { sendCommand(btn.toLowerCase()); }); document.getElementById('btn' + btn).addEventListener('touchstart', (e) => { e.preventDefault(); sendCommand(btn.toLowerCase()); }); }); // 按钮释放时发送停止命令(除了Stop按钮) ['Forward', 'Backward', 'Left', 'Right'].forEach(btn => { const element = document.getElementById('btn' + btn); const releaseHandler = () => { if(currentStatus !== 'stop') sendCommand('stop'); }; element.addEventListener('mouseup', releaseHandler); element.addEventListener('touchend', (e) => { e.preventDefault(); releaseHandler(); }); element.addEventListener('mouseleave', releaseHandler); }); // 键盘控制支持 document.addEventListener('keydown', (e) => { switch(e.key) { case 'ArrowUp': case 'w': case 'W': sendCommand('forward'); break; case 'ArrowDown': case 's': case 'S': sendCommand('backward'); break; case 'ArrowLeft': case 'a': case 'A': sendCommand('left'); break; case 'ArrowRight': case 'd': case 'D': sendCommand('right'); break; case ' ': sendCommand('stop'); break; } }); document.addEventListener('keyup', (e) => { if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','w','W','s','S','a','A','d','D'].includes(e.key)) { sendCommand('stop'); } }); </script> </body> </html> ''' @app.route('/') def index(): # 获取树莓派的本机IP,方便手机连接 import socket hostname = socket.gethostname() local_ip = socket.gethostbyname(hostname) return render_template_string(HTML_PAGE, ip=local_ip) @app.route('/cmd/<command>', methods=['POST']) def control_car(command): global current_cmd with cmd_lock: if command == 'forward': car.move_forward(0.6) current_cmd = 'forward' elif command == 'backward': car.move_backward(0.6) current_cmd = 'backward' elif command == 'left': car.turn_left(0.5) current_cmd = 'left' elif command == 'right': car.turn_right(0.5) current_cmd = 'right' elif command == 'stop': car.stop() current_cmd = 'stop' else: return jsonify({'error': '未知命令'}), 400 return jsonify({'status': 'ok', 'command': current_cmd}) def run_car_controller(): """后台线程,处理连续命令(可选)""" pass # 目前是即时命令,如需持续运动可在此实现 if __name__ == '__main__': # 启动后台控制线程 controller_thread = threading.Thread(target=run_car_controller, daemon=True) controller_thread.start() print("Web遥控服务器启动中...") print("在浏览器中访问树莓派的IP地址即可控制小车") print("例如: http://192.168.1.100:5000") # 注意:host='0.0.0.0' 允许所有网络接口访问,debug=True仅用于开发 app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)运行这个脚本后,在同一个局域网的手机或电脑浏览器中,输入树莓派的IP地址加:5000端口(如http://192.168.1.100:5000),就能看到一个遥控界面,通过点击按钮或键盘即可控制小车。
6.3 其他扩展方向
- 巡线功能:添加2-3个红外反射传感器(TCRT5000)安装在底盘前部,通过检测地面黑线与白底的反射光强度差,实现自动沿着黑色轨迹线行驶。
- 视频图传:为树莓派连接一个CSI摄像头或USB摄像头,使用
picamera2或OpenCV库捕获视频流,再通过Flask建立一个视频流服务器,就可以在遥控网页上实时看到小车前方的画面,实现第一人称视角驾驶。 - 环境监测:添加DHT11温湿度传感器、MQ-2烟雾传感器等,让小车在移动中采集环境数据,并通过网络发送到服务器或显示在网页上。
- 自动驾驶:结合摄像头和机器学习框架(如TensorFlow Lite),可以在树莓派上运行轻量级模型,实现车道线识别、交通标志识别等更高级的自动驾驶功能。
7. 常见问题排查与调试心得
在制作和调试过程中,你几乎一定会遇到下面这些问题。这里我把踩过的坑和解决方法总结出来,希望能帮你节省大量时间。
7.1 硬件连接问题
问题1:上电后树莓派指示灯不亮,或亮一下即灭。
- 可能原因:电源问题。树莓派3/4需要稳定5V/2.5A以上的电源。使用手机充电器或电脑USB口供电可能电流不足。
- 排查:使用官方电源或标称输出5V/3A的优质电源适配器。检查Micro USB/USB-C线是否完好,劣质线缆内阻过大也会导致供电不足。
问题2:电机不转,或只有一个电机转。
- 排查步骤:
- 检查电源:用万用表测量电池盒输出电压是否在5.5V以上(新电池应接近6V)。电量不足的电池无法驱动电机。
- 检查接线:这是最常见的问题。逐根检查树莓派到驱动板、驱动板到电机、电池盒到驱动板的每一根线是否连接牢固,是否接错了引脚。重点检查共地线(GND)是否连接。
- 交换测试:将不转的电机接到正常工作的电机接口上,如果转了,说明电机是好的,问题在驱动板该通道或树莓派对应的GPIO上。如果不转,则可能是电机本身损坏或接线断路。
- 代码测试:写一个最简单的测试程序,逐个让每个电机正转、反转,同时用万用表测量驱动板对应输出端是否有电压变化。如果没有,则检查树莓派GPIO输出是否正常。
问题3:小车运动时树莓派频繁重启或断开连接。
- 根本原因:电机工作时产生的瞬间大电流,通过共地线“污染”了树莓派的电源,导致其电压不稳(俗称“电压毛刺”)。
- 解决方案:
- 电源隔离:确保树莓派和电机使用完全独立的两组电池或电源,这是最有效的方法。
- 加装电容:在电机驱动板的电源输入引脚(VM和GND之间)并联一个大容量电解电容(如470μF 16V)和一个小容量陶瓷电容(如0.1μF)。大电容缓冲电流突变,小电容滤除高频噪声。
- 使用稳压模块:如果必须共用一组电池(如一个大的锂电池),务必使用DC-DC降压模块(如LM2596)为树莓派提供稳定、干净的5V电压,绝不能直接连接。
7.2 软件与编程问题
问题4:运行Python程序时提示ImportError: No module named 'gpiozero'
- 原因:未安装
gpiozero库,或在Python2环境下运行(树莓派OS默认Python3)。 - 解决:确保使用
python3命令运行脚本,并已通过sudo apt install python3-gpiozero安装库。
问题5:提示GPIO already in use错误。
- 原因:之前的程序异常退出,没有正确清理GPIO资源,或者另一个程序正在使用相同的GPIO引脚。
- 解决:
- 重启树莓派,这是最彻底的方法。
- 在终端输入
sudo killall python3结束所有Python进程。 - 在代码中务必使用
try...except...finally结构,并在finally块中调用清理函数。
问题6:PWM调速不线性,低速时电机抖动或不转。
- 原因:电机有启动电压阈值,PWM占空比太低时,等效电压不足以克服静摩擦力启动电机。
- 解决:
- 设置死区:在代码中,将速度小于某个值(如0.2)时直接视为0(停止)。
- 提高PWM频率:
gpiozero中PWMOutputDevice的默认频率可能较低(如100Hz)。尝试提高到500Hz或1000Hz,电机运行会更平滑。修改Motor类初始化中的frequency参数。 - 使用更先进的驱动芯片:如之前提到的TB6612FNG比L298N在低速控制上表现更好。
7.3 机械与结构问题
问题7:小车跑不直。
- 原因:两个电机的实际转速有细微差异,即使代码里给的PWM值相同。
- 解决:
- 软件校准:在代码中为两个电机设置一个微调系数。例如,如果小车总是右偏,可以稍微降低左电机的速度系数
left_speed_factor = 0.95,或在move_forward函数中让右轮速度略低于左轮,直到它能跑直。这需要反复测试。 - 硬件检查:检查两个轮子是否安装牢固、直径是否完全相同、底盘是否对称、万向轮是否顺滑。
- 软件校准:在代码中为两个电机设置一个微调系数。例如,如果小车总是右偏,可以稍微降低左电机的速度系数
问题8:车轮打滑或抓地力不足。
- 原因:轮子材质太硬或地面太光滑。
- 解决:更换为橡胶材质的轮子,或者在现有轮子上套上一圈橡胶圈(如气门芯胶管)来增加摩擦力。
问题9:整体结构松散,行驶时晃动。
- 原因:使用胶带或不够牢固的方式固定部件。
- 解决:使用螺丝、螺母、尼龙柱和扎带进行固定。投资一个简单的智能小车底盘套件能省去很多麻烦,上面通常有标准的安装孔位。
这个项目最迷人的地方在于,它既是一个明确的终点——你成功制作了一辆可控的小车;更是一个充满可能性的起点——你可以根据自己的想法,无限地扩展它的功能。从简单的避障到复杂的视觉识别,每一次代码的修改和硬件的添加,都是你对嵌入式世界更深一次的探索。调试过程中遇到的每一个问题,其解决过程带来的经验,远比最终成功跑起来的那一下更宝贵。当你看到自己编写的一行行代码,精确地控制着物理世界中的车轮旋转时,那种跨越虚拟与现实的创造感,正是创客精神的精髓所在。
