1. 从零到一:为什么选择行空板作为遥控小车的“大脑”?
做一辆遥控小车,听起来像是电子爱好者的入门项目。但当你真正动手时,会发现选择“大脑”是个让人纠结的问题。是Arduino Uno?树莓派Pico?还是更强大的树莓派4B?我试过不少方案,直到用上了行空板,才感觉找到了那个“刚刚好”的平衡点。今天,我就来聊聊如何用行空板,从零开始打造一辆功能全面、扩展性强的物联遥控小车,并分享那些在教程里不会写的实操细节。
行空板本质上是一块集成了高性能处理器、Wi-Fi/蓝牙、触摸屏、多种传感器和丰富GPIO接口的单板计算机。它和我们熟悉的树莓派类似,但开箱即用的体验和针对教育、物联网场景的优化,让它在这个项目里显得格外顺手。你可能会问,用Arduino加个电机驱动板不也能跑吗?确实可以,但那就只是一个简单的遥控车了。而行空板带来的,是“物联”和“智能”的可能性——你可以通过网页远程控制、可以编程实现自动避障、可以上传传感器数据到云端、甚至可以让小车识别物体。这一切,都因为行空板本身就是一个完整的、可以运行Python等高级语言的微型电脑。
这个项目的核心价值,在于它不是一个简单的“接线-烧录”练习,而是一个完整的软硬件集成项目。你将接触到硬件接线、电机驱动原理、无线通信协议(如MQTT或WebSocket)、Web服务器搭建、以及前后端交互。无论你是想学习物联网开发,还是为某个创意项目寻找一个移动载体,这辆小车都能提供一个绝佳的实践平台。接下来,我会带你一步步拆解,从硬件选型、电路连接,到软件架构、代码编写,最后是调试优化和功能扩展,把每个环节的“为什么”和“怎么做”都讲清楚。
2. 硬件清单与核心电路设计:不止于“能动起来”
动手之前,理清硬件清单是避免反复跑电子市场的最好方法。我们的目标是做一辆结实、稳定、便于扩展的小车,所以在部件选择上要有一些考量。
2.1 核心部件选型与考量
-
主控:行空板
。这是项目的核心。我选择的是行空板标准版,它的双核处理器、512MB内存和内置的Wi-Fi模块,对于运行一个Python控制程序和小型Web服务器绰绰有余。其板载的加速度计、光线传感器等,也为后续的智能功能(如跌倒检测、光感巡线)预留了空间。
-
小车底盘套件
:建议直接购买四驱麦克纳姆轮底盘套件。为什么是麦克纳姆轮?因为它可以实现全向移动(前后、左右、原地旋转),这让遥控体验和后续的路径规划算法实现起来有趣得多。相比普通的差速轮,虽然价格稍高,但带来的操控自由度是质的飞跃。套件通常包含底盘、4个电机、4个轮子和必要的螺丝。
-
电机驱动模块
:这是连接行空板GPIO(弱电)和电机(强电)的关键桥梁。行空板的GPIO引脚只能提供很小的电流(通常20mA左右),根本无法驱动电机。因此,我们必须使用电机驱动模块。我选用的是
L298N双H桥直流电机驱动模块
。选择它的理由很充分:首先,它非常经典,资料极多,社区支持好;其次,一个L298N可以同时驱动两个直流电机,我们的小车有四个电机,因此需要两个L298N模块;最后,它的驱动电流(单桥2A)足以应对我们小车用的N20这类小型减速电机。
-
电源系统
:这是稳定性的基石。
绝对不能
尝试用行空板的USB口给整个系统供电,电流远远不够。我们需要独立的动力电源。方案如下:
-
行空板供电
:一块普通的手机充电宝(5V/2A)通过USB Type-C接口给行空板供电,稳定可靠。
-
电机驱动供电
:需要另一组电源。我推荐使用
两节18650锂电池串联的电池盒
(输出约7.4V)。这个电压对于L298N和N20电机来说非常合适。
切记
:务必为这组电池配备一个对应的充电模块,保证安全。
-
-
其他辅料
:杜邦线(公对公、公对母若干)、扎带、螺丝刀、可能需要的扩展板(用于更方便地连接行空板GPIO)等。
2.2 电路连接详解:理清逻辑,避免短路
接线是硬件部分最容易出错的地方。遵循“先逻辑,后物理”的原则,我们先理解信号流向:行空板GPIO发出控制信号(高低电平)给L298N,L298N根据这些信号控制电源(电池)对电机的通断和方向,从而驱动电机。
下面以控制两个电机(例如右前和右后,它们由一个L298N控制)为例,详解接线步骤。另一个L298N控制左侧两个电机,接法完全相同。
注意:在连接任何线路前,确保所有电源处于断开状态!先接信号线,最后再接电源。
连接L298N与电机
:将右前、右后两个电机的线,分别接入一个L298N模块的
OUT1/OUT2
和
OUT3/OUT4
端子。电机线不分正负,接反了只会导致转向相反,后期在软件中调整即可。
连接L298N与行空板(控制信号)
:
-
L298N上有
IN1, IN2, IN3, IN4
四个输入引脚,它们接收来自行空板的控制信号。
-
假设我们分配行空板的
D18
引脚给
IN1
,
D19
给
IN2
(控制右前电机),
D22
给
IN3
,
D23
给
IN4
(控制右后电机)。用杜邦线(公对公)将这些引脚连接起来。
-
L298N上还有
ENA
和
ENB
两个使能引脚。将它们分别接到行空板的
PWM
引脚上,例如
D12
和
D13
。PWM信号可以让我们控制电机的速度,而不仅仅是开关。如果不接PWM,直接将
ENA/ENB
跳线帽接到
5V
上,则电机只能全速或停止。
连接电源
:
-
电机驱动电源
:将7.4V的18650电池盒的正极(+)接到L298N的
+12V
输入端子(虽然标12V,但7.4V完全可用),负极(-)接到
GND
端子。同时,从这个
GND
引出一根线,连接到行空板的任何一个
GND
引脚。
这一步至关重要!
它叫“共地”,目的是让行空板和L298N有一个共同的电压参考点,否则控制信号会失效甚至损坏设备。
-
L298N逻辑电源
:L298N上还有一个
+5V
端子,它可以输出5V,也可以输入5V为板载逻辑电路供电。当我们的驱动电源(7.4V)高于5V时,板载稳压芯片会工作,此时
+5V
端子可以作为5V输出。我们不需要用它,但务必确保其跳线帽在位(如果模块有的话)。
同理,连接第二个L298N
,控制左侧两个电机,并分配好行空板上对应的控制引脚和PWM引脚。
完成后的核心信号连接关系可以总结如下表,方便核对:
| 右前电机 | D18 -> IN1, D19 -> IN2 | D12 -> ENA | OUT1, OUT2 |
| 右后电机 | D22 -> IN3, D23 -> IN4 | D13 -> ENB | OUT3, OUT4 |
| 左前电机 | D24 -> IN1, D25 -> IN2 | D26 -> ENA | OUT1, OUT2 |
| 左后电机 | D27 -> IN3, D28 -> IN4 | D29 -> ENB | OUT3, OUT4 |
实操心得:接线完成后,先不要组装到底盘上。用扎带或胶带简单固定各模块,进行上电前的“静态测试”。编写一个最简单的Python脚本,让某个电机正转1秒、停止1秒、反转1秒。确认单个电机单元工作正常后,再进行整体组装和复杂逻辑测试。这能有效隔离问题,避免全部装好后排查困难。
3. 软件架构与核心控制逻辑:让小车“听懂人话”
硬件是躯干,软件才是灵魂。我们的软件需要完成两件大事:第一,接收来自遥控端(如网页)的指令;第二,将这些指令翻译成具体的电机动作。这里我采用一个非常清晰的三层架构:
Web服务器层
、
指令解析层
和
电机驱动层
。
3.1 搭建一个轻量级Web遥控器
我们选择在行空板上直接用Python运行一个Web服务器,这样任何连接到同一局域网的设备(手机、电脑)打开浏览器就能遥控,无需安装任何APP。
Flask
是一个极简的Python Web框架,非常适合这个任务。
首先,通过行空板的终端或IDE安装Flask:
pip install flask
。然后创建主程序文件,比如
car_control.py
。
# car_control.py – Web服务器与主控制逻辑
from flask import Flask, render_template_string, request, jsonify
import RPi.GPIO as GPIO
import time
# 初始化Flask应用
app = Flask(__name__)
# ========== 电机驱动层 ==========
# 引脚定义 (根据之前的接线表)
# 右侧电机
RIGHT_FRONT_IN1, RIGHT_FRONT_IN2 = 18, 19
RIGHT_FRONT_ENA = 12
RIGHT_REAR_IN3, RIGHT_REAR_IN4 = 22, 23
RIGHT_REAR_ENB = 13
# 左侧电机
LEFT_FRONT_IN1, LEFT_FRONT_IN2 = 24, 25
LEFT_FRONT_ENA = 26
LEFT_REAR_IN3, LEFT_REAR_IN4 = 27, 28
LEFT_REAR_ENB = 29
# 所有电机控制引脚列表
MOTOR_PINS = [RIGHT_FRONT_IN1, RIGHT_FRONT_IN2, RIGHT_FRONT_ENA,
RIGHT_REAR_IN3, RIGHT_REAR_IN4, RIGHT_REAR_ENB,
LEFT_FRONT_IN1, LEFT_FRONT_IN2, LEFT_FRONT_ENA,
LEFT_REAR_IN3, LEFT_REAR_IN4, LEFT_REAR_ENB]
# 初始化GPIO
GPIO.setmode(GPIO.BCM) # 使用BCM编号
GPIO.setwarnings(False)
for pin in MOTOR_PINS:
GPIO.setup(pin, GPIO.OUT)
GPIO.output(pin, GPIO.LOW)
# 初始化PWM对象用于速度控制
pwm_rf = GPIO.PWM(RIGHT_FRONT_ENA, 1000) # 频率1000Hz
pwm_rr = GPIO.PWM(RIGHT_REAR_ENB, 1000)
pwm_lf = GPIO.PWM(LEFT_FRONT_ENA, 1000)
pwm_lr = GPIO.PWM(LEFT_REAR_ENB, 1000)
# 启动PWM,初始占空比为0(停止)
pwm_rf.start(0)
pwm_rr.start(0)
pwm_lf.start(0)
pwm_lr.start(0)
# 电机控制函数
def set_motor_speed(pwm_obj, speed):
"""设置指定PWM对象的速度,speed范围0-100"""
pwm_obj.ChangeDutyCycle(speed)
def motor_stop():
"""停止所有电机"""
GPIO.output(RIGHT_FRONT_IN1, GPIO.LOW)
GPIO.output(RIGHT_FRONT_IN2, GPIO.LOW)
# … 同理设置所有IN引脚为LOW
set_motor_speed(pwm_rf, 0)
set_motor_speed(pwm_rr, 0)
set_motor_speed(pwm_lf, 0)
set_motor_speed(pwm_lr, 0)
def move_forward(speed=50):
"""前进:所有电机正转"""
# 右侧电机正转 (IN1=HIGH, IN2=LOW)
GPIO.output(RIGHT_FRONT_IN1, GPIO.HIGH)
GPIO.output(RIGHT_FRONT_IN2, GPIO.LOW)
GPIO.output(RIGHT_REAR_IN3, GPIO.HIGH)
GPIO.output(RIGHT_REAR_IN4, GPIO.LOW)
# 左侧电机正转 (IN1=HIGH, IN2=LOW)
GPIO.output(LEFT_FRONT_IN1, GPIO.HIGH)
GPIO.output(LEFT_FRONT_IN2, GPIO.LOW)
GPIO.output(LEFT_REAR_IN3, GPIO.HIGH)
GPIO.output(LEFT_REAR_IN4, GPIO.LOW)
# 设置速度
set_motor_speed(pwm_rf, speed)
set_motor_speed(pwm_rr, speed)
set_motor_speed(pwm_lf, speed)
set_motor_speed(pwm_lr, speed)
# 类似地,实现move_backward, turn_left, turn_right, strafe_left, strafe_right等函数
# 这里以原地左转为例:
def rotate_left(speed=50):
"""原地左转:右侧电机正转,左侧电机反转"""
# 右侧正转
GPIO.output(RIGHT_FRONT_IN1, GPIO.HIGH)
GPIO.output(RIGHT_FRONT_IN2, GPIO.LOW)
GPIO.output(RIGHT_REAR_IN3, GPIO.HIGH)
GPIO.output(RIGHT_REAR_IN4, GPIO.LOW)
# 左侧反转
GPIO.output(LEFT_FRONT_IN1, GPIO.LOW)
GPIO.output(LEFT_FRONT_IN2, GPIO.HIGH)
GPIO.output(LEFT_REAR_IN3, GPIO.LOW)
GPIO.output(LEFT_REAR_IN4, GPIO.HIGH)
set_motor_speed(pwm_rf, speed)
set_motor_speed(pwm_rr, speed)
set_motor_speed(pwm_lf, speed)
set_motor_speed(pwm_lr, speed)
# ========== 指令解析层 ==========
def execute_command(cmd, speed=50):
"""根据字符串指令执行对应的动作"""
if cmd == 'forward':
move_forward(speed)
elif cmd == 'backward':
move_backward(speed)
elif cmd == 'left':
rotate_left(speed)
elif cmd == 'right':
rotate_right(speed)
elif cmd == 'stop':
motor_stop()
# 可以继续添加其他指令,如‘strafe_left’(左平移)
else:
motor_stop() # 未知指令则停止
# ========== Web服务器层 ==========
# 一个简单的HTML页面,包含控制按钮
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html>
<head>
<title>行空板小车遥控器</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { text-align: center; font-family: Arial; }
.control-panel { margin: 20px auto; width: 300px; }
.btn {
width: 80px; height: 80px; margin: 5px; font-size: 24px;
border: none; border-radius: 10px; background: #4CAF50; color: white;
}
.btn:active { background: #45a049; }
#stopBtn { background: #f44336; width: 180px; }
#stopBtn:active { background: #d32f2f; }
</style>
</head>
<body>
<h2>行空板物联小车遥控器</h2>
<div>速度: <input type="range" id="speedSlider" min="0" max="100" value="50"> <span id="speedValue">50</span>%</div>
<div class="control-panel">
<div><button class="btn" onmousedown="sendCmd('forward')" ontouchstart="sendCmd('forward')" onmouseup="sendCmd('stop')" ontouchend="sendCmd('stop')">↑</button></div>
<div>
<button class="btn" onmousedown="sendCmd('left')" ontouchstart="sendCmd('left')" onmouseup="sendCmd('stop')" ontouchend="sendCmd('stop')">←</button>
<button id="stopBtn" class="btn" onclick="sendCmd('stop')">停</button>
<button class="btn" onmousedown="sendCmd('right')" ontouchstart="sendCmd('right')" onmouseup="sendCmd('stop')" ontouchend="sendCmd('stop')">→</button>
</div>
<div><button class="btn" onmousedown="sendCmd('backward')" ontouchstart="sendCmd('backward')" onmouseup="sendCmd('stop')" ontouchend="sendCmd('stop')">↓</button></div>
</div>
<script>
let speed = 50;
const speedSlider = document.getElementById('speedSlider');
const speedValue = document.getElementById('speedValue');
speedSlider.oninput = function() { speed = this.value; speedValue.textContent = speed; }
function sendCmd(cmd) {
fetch(`/api/control?cmd=${cmd}&speed=${speed}`)
.then(response => response.json())
.then(data => console.log(data.status));
}
// 键盘控制
document.addEventListener('keydown', (e) => {
let cmd = '';
switch(e.key) {
case 'ArrowUp': cmd='forward'; break;
case 'ArrowDown': cmd='backward'; break;
case 'ArrowLeft': cmd='left'; break;
case 'ArrowRight': cmd='right'; break;
case ' ': cmd='stop'; break;
}
if(cmd) sendCmd(cmd);
});
document.addEventListener('keyup', (e) => {
if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) {
sendCmd('stop');
}
});
</script>
</body>
</html>
'''
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE)
@app.route('/api/control')
def api_control():
cmd = request.args.get('cmd', 'stop')
speed = int(request.args.get('speed', 50))
execute_command(cmd, speed)
return jsonify({'status': 'OK', 'command': cmd, 'speed': speed})
if __name__ == '__main__':
try:
print("小车控制服务器启动中…")
# 获取行空板IP,方便连接
import socket
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
print(f"请在同一网络下的设备浏览器中访问: http://{local_ip}:5000")
app.run(host='0.0.0.0', port=5000, debug=False, threaded=True) # debug=False用于生产环境
except KeyboardInterrupt:
print("程序被中断")
finally:
motor_stop()
GPIO.cleanup()
这段代码是一个完整的、可运行的原型。它包含了三层架构:Flask提供了Web界面和API接口(服务器层);
/api/control
接口接收到指令后,调用
execute_command
函数(解析层);该函数再调用具体的
move_forward
等函数(驱动层),最终操作GPIO和PWM控制电机。
3.2 麦克纳姆轮运动解算:让平移成为可能
如果你使用的是普通差速轮,那么前进、后退、左转、右转的逻辑相对简单。但既然我们用了麦克纳姆轮,就必须实现“平移”这种更酷的运动。这就需要一点简单的向量分解知识。
一个麦克纳姆轮小车,其运动可以分解为三个自由度:
沿X轴平移(左右)
、
沿Y轴平移(前后)
和
绕中心旋转
。每个轮子的转速和方向,都是这三个基本运动向量的合成。
假设四个轮子分别位于小车的四个角:右前(RF)、左前(LF)、右后(RR)、左后(LR)。当我们希望小车向右平移时,右前和左后轮需要向前转,左前和右后轮需要向后转。这个逻辑是固定的,我们可以将其预先计算好,封装成一个函数。
在代码中,我们可以扩展
execute_command
函数,并添加一个更底层的
set_motors
函数来处理这种合成:
def set_motors(vx, vy, omega, max_speed=100):
"""
设置小车运动。
vx: X轴速度(右为正),范围 -100 到 100
vy: Y轴速度(前为正),范围 -100 到 100
omega: 旋转速度(逆时针为正),范围 -100 到 100
max_speed: 最大合成速度限制
"""
# 这是针对麦克纳姆轮布局的运动学公式(一个简化版)
# 不同轮子布局系数可能不同,需要根据实际调整
rf_power = vy – vx – omega
lf_power = vy + vx + omega
rr_power = vy + vx – omega
lr_power = vy – vx + omega
# 归一化,防止任何电机功率超过max_speed
powers = [rf_power, lf_power, rr_power, lr_power]
max_power = max(abs(p) for p in powers)
if max_power > max_speed:
scale = max_speed / max_power
rf_power *= scale
lf_power *= scale
rr_power *= scale
lr_power *= scale
# 根据计算出的功率值,设置每个电机的方向和PWM
# 这里需要将功率值映射到具体的电机控制函数,略复杂,但逻辑是:
# 如果power为正,则电机正转;为负则反转;绝对值大小决定PWM占空比。
# 具体实现需要根据你的电机接线方向做调整,可能需要一个`set_single_motor(motor_id, power)`函数。
这样,当遥控端发送
{vx: 50, vy: 0, omega: 0}
时,小车就会向右平移。网页上的控制按钮可以从简单的方向键,升级为虚拟摇杆,直接发送
vx, vy, omega
这三个值,操控体验会直接上升到“游戏手柄”级别。
4. 系统集成、调试与功能扩展:从“能动”到“好用”
将硬件组装好,代码上传到行空板并运行后,你的小车应该已经能通过网页遥控了。但这只是开始,接下来才是体现工程能力的地方:让系统稳定、可靠,并具备扩展潜力。
4.1 上电与网络连接调试
首先,确保所有接线牢固。先给行空板(充电宝)上电,等待系统启动。通过行空板的屏幕或连接其热点,获取它的IP地址。然后在电脑浏览器输入
http://<行空板IP>:5000
,应该能看到遥控界面。
常见问题1:网页能打开,但点击按钮小车没反应。
-
排查思路
:
-
检查后台日志
:在运行
car_control.py
的终端里,查看当你点击按钮时,是否有
/api/control
的访问日志。如果没有,可能是前端JavaScript有问题,检查浏览器控制台(F12)的报错。
-
检查GPIO状态
:在代码中添加调试语句,打印出收到的
cmd
和
speed
,并打印出执行
execute_command
后各个GPIO引脚的状态。用万用表测量相应GPIO引脚是否有电压变化。
-
检查电源与共地
:这是最容易被忽略的。确保L298N的电机电源已接通,并且L298N的GND和行空板的GND已经用导线连接在一起。没有共地,信号无法形成回路。
-
检查电机和接线
:直接将电机线接到电池上,看电机是否正常转动,排除电机本身故障。检查杜邦线是否虚接。
常见问题2:小车运动方向与预期相反。
-
解决方案
:这是电机线接反或程序里的正反转逻辑设反了。不要重新接线,太麻烦。直接在软件里调整。例如,如果前进时小车后退,只需将
move_forward
函数中所有
GPIO.HIGH
和
GPIO.LOW
对调即可。这是软件控制硬件的优势。
4.2 优化操控体验与稳定性
-
指令去抖与连续控制
:当前的代码在按钮按下时发送一次指令,松开时发送停止。但在快速操作时,网络延迟可能导致指令堆积或响应不及时。更好的做法是:前端以固定频率(如每秒10次)向后台发送当前摇杆状态,后台持续根据最新状态调整电机。这需要将API从
GET
改为
POST
,并维护一个最新的运动状态变量。
-
速度曲线与加速度
:直接让电机从0到全速启动,小车会非常“冲”。可以引入速度斜坡函数,让速度平滑变化,提升操控手感。
-
电池电压监测
:行空板有ADC引脚,可以配合分压电路读取电池电压。在网页遥控界面上显示实时电压,当电压过低时报警,防止电池过放损坏。
-
异常处理与安全停止
:在代码中捕获异常(如网络断开、GPIO错误),并在
finally
块中确保调用
motor_stop()
和
GPIO.cleanup()
。可以设置一个看门狗线程,如果超过一定时间没收到遥控指令,自动停止小车。
4.3 迈向真正的“物联”:功能扩展思路
当基础遥控功能稳定后,行空板的潜力才真正发挥出来。
-
视频图传
:连接一个USB摄像头或CSI摄像头到行空板,使用
MJPG-Streamer
或
Flask-VideoStreaming
等库,在遥控网页上实时显示小车前方的视频流,实现第一人称视角(FPV)驾驶。
-
自动避障
:在小车前方加装HC-SR04超声波模块或TOF激光测距传感器。在Python程序中创建一个避障线程,实时测量前方距离。当距离小于安全阈值时,自动覆盖遥控指令,执行刹车或绕行逻辑。
-
传感器数据可视化
:利用行空板自带的传感器(加速度计、光线传感器),或外接温湿度传感器,将数据通过WebSocket实时推送到网页前端,绘制成动态图表。
-
离线语音控制
:接入一个USB麦克风,利用
SpeechRecognition
库进行离线关键词识别(如“前进”、“左转”、“停”),实现语音控制。
-
云端连接与远程控制
:使用行空板的Wi-Fi,通过MQTT协议连接到公共或私有的物联网平台(如EMQX、阿里云IoT)。这样,即使你不在同一个局域网,只要能上网,就可以远程控制小车,并接收传感器数据。这才是完整的“物联网小车”。
从一堆散件到一辆能听你指挥、甚至能自主思考的智能小车,这个过程充满挑战也极具成就感。行空板降低了嵌入式智能设备开发的门槛,让你能更专注于逻辑和功能的实现。最关键的是,在调试电机驱动、解决电源干扰、优化控制算法的过程中积累的经验,远比最终让小车轮子转起来本身更有价值。


