利用树莓派做智能小车是个很常见的玩法,整个过程涉及手工制作、GPIO控制、Python程序编写、网络通信等内容,知乎上有的大神还加入图像识别甚至人工智能元素,我自己在制作过程中真的感觉非常有意思,也很有成就感。为了做这个小车,我不惜破费买了各种小车零件和电子元器件(其实花不了多少钱),还突击学习了Python,参考书上的内容和网络上的信息也搞出了自己的小车,虽然功能真的很简单,这次记录的是简单的避障功能实现。

1 小车结构

整个小车涉及的元件:
①树莓派,作为主控芯片;
②小车底盘及其他配件,淘宝上30块钱就能买到;
③L298N 电机驱动模块,用来控制小车电机;
④HC-SR04 超声波测距模块,用来检测距离;
⑤干电池组或者锂电池,为L298N供电,需要12V左右;
⑥杜邦线、导线、螺丝刀等工具。

下图是整体的结构示意,小车的每个轮子都装有电机,电机与L298N模块相连,而树莓派控制L298N模块进而控制电机运动,同时树莓派也控制超声波测距模块检测距离。

2 树莓派GPIO概念

之所以树莓派可以直接控制电子元件,是因为它提供了GPIO区,所谓GPIO就是可以自由配置引脚的模式,比如可以配置成输出模式从而输出高低电平,或者可以配置成输入模式接收电平信号,正因为树莓派的GPIO功能,让它看起来似乎更加底层化了。

使用GPIO前首先需要了解的是引脚的编号方式,如下图所示,按照物理方位的话是按照从左到右、从上到下的方式从1编号到40,但是在编写程序是一般不用物理编码方式,如果用C语言写的话需要导入wiringPi库,用Python写的话需要导入RPi.GPIO库,这两种库的引脚编号方式都不同,例如按照RPi.GPIO编号方式,物理号码为38的引脚编号为BCM.20,而物理号码为40的引脚编号为BCM.21。完整的编码表可以查阅网上资料。

Python程序下涉及GPIO的主要代码摘录如下:

import RPi.GPIO as GPIO #导入GPIO模块
GPIO.setmode(GPIO.BCM) #设定编号模式
GPIO.setwarnings(False) #关闭警告说明
GPIO.setup(1, GPIO.IN) #设置引脚1(BCM编号)为输入通道
GPIO.setup(2, GPIO.OUT) #设置引脚2为输出通道
value=GPIO.input(1) #读取通道1的输入值( 0 / GPIO.LOW / False 或者 1 / GPIO.HIGH / True)
GPIO.output(2, GPIO.HIGH) #设置通道2输出高电平的状态,状态可以为0 / GPIO.LOW / False 或者 1 / GPIO.HIGH / True
GPIO.cleanup() #清理通道资源

3 车体拼装

首先把小车车架拼好,买来的车架是有说明书的,而且车架本身也很简单,用固定片把各个电机固定好就基本OK了,需要注意的是马达朝向、螺丝朝向等小细节,有时候朝向不对会卡住轮子,或者不方便后续的绕线等操作。下图是我拼装好的车架图(这里已经把电机控制模块和测距模块加上去了)。

4 测距模块

超声波模块利用超声波反射检测距离,测量范围较小但是精度较高。模块工作电压5v,使用时首先需要在trig引脚上输入一个长为20us的高电平方波,这样会触发模块发射8个40kHz的超声波,同时echo引脚的电平会由0变为1。当超声波返回被接收时,echo的电平由1变为0。用定时器记录下这个时长t,则距离=340*t/2。
根据工作原理编写超声波测距模块的Python文件:

import RPi.GPIO as GPIO
import time
class Ultrasonic_Module(object):
    '''超声波测距模块'''
    def __init__(self,trig_pin,echo_pin):
        self.trig_pin=trig_pin
        self.echo_pin=echo_pin
        self.pins_for_motor=[5,6,13,19,21,22,23,24] #这些引脚被电机模块占用了
        self.pins_available=[12,16,17,18,20,25,26,27]

    def setup(self):
        '''引脚初始化'''
        if (self.trig_pin not in self.pins_for_motor) and (self.trig_pin in self.pins_available):
            print("trig_pin is vaild")
        else:
            print("trig_pin is invalid")
            return False

        if (self.echo_pin not in self.pins_for_motor) and (self.echo_pin in self.pins_available):
            print("echo_pin is vaild")
        else:
            print("echo_pin is invalid")
            return False

        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)
        GPIO.setup(self.trig_pin,GPIO.OUT,initial=GPIO.LOW)
        GPIO.setup(self.echo_pin,GPIO.IN)
        return True

    def getdistance(self):
        '''轮询方法获取距离'''
        GPIO.output(self.trig_pin,GPIO.HIGH)
        time.sleep(0.000015)
        GPIO.output(self.trig_pin,GPIO.LOW)

        while not GPIO.input(self.echo_pin):
            pass
        t1=time.time() #开始计时
        while GPIO.input(self.echo_pin):
            pass
        t2=time.time()
        distance=(t2-t1)*340/2
        return distance

代码中获取距离是用简单的轮询方法实现的,也可以使用GPIO中的上/下沿检测加回调方法的手段实现。

5 电机模块

用L298N模块控制马达运动,首先需要了解L298N的工作逻辑。

如上图所示,左下角的三个接线柱是12V电源输入、GND和5V输出;左右的接线柱分别控制两个电机,OUT1和OUT2控制左边的马达,OUT3和OUT4控制右边的马达;右下角的接线柱是使能及控制脚,ENA、IN1和IN2为左侧电机的控制组,控制OUT1和OUT2的输出逻辑,ENB、IN3和IN4为右侧控制组,控制OUT3和OUT4。以左侧控制组为例,下表是其真值表:

此外,如果给使能脚提供PWM波,则电机转速可调。

正常情况下是一个L298N带2个马达,一左一右,现在小车四个轮子都有马达,因此可以将一侧的两个马达并联,这样一个L298N带4个马达,只要电池电力足够4个马达是可以带动的(当然也可以再加一个L298N)。马达连线时需要注意一侧的两个不要接反了,因为要保证一侧的两个运动方向一致。

小车的运动逻辑还是很简单的,两侧同时正转就是前进,同时反转就是后退,左侧不动右侧正转就是左转弯,右侧不动左侧正转就是右转弯。由此编写电机控制模块的Python文件:

import RPi.GPIO as GPIO
import time
import sys

class Motor_Module(object):
    '''电机控制模块'''
    def __init__(self):
        self.enab_pin=[5,6,13,19] #使能脚编号
        self.inx_pin=[21,22,23,24] #控制脚编号

        self.RightAhead_pin=self.inx_pin[3]
        self.RightBack_pin=self.inx_pin[2]
        self.LeftAhead_pin=self.inx_pin[1]
        self.LeftBack_pin=self.inx_pin[0]

        self.setup()

    def setup(self):
        '''引脚初始化'''
        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)
        for pin in self.inx_pin:
            GPIO.setup(pin,GPIO.OUT)
            GPIO.output(pin,GPIO.LOW)
        for pin in self.enab_pin:
            GPIO.setup(pin,GPIO.OUT)
            GPIO.output(pin,GPIO.HIGH)

    def ahead(self,secondvalue=0):
        self.setup()
        GPIO.output(self.RightAhead_pin,GPIO.HIGH)
        GPIO.output(self.LeftAhead_pin,GPIO.HIGH)
        if secondvalue!=0:
           time.sleep(secondvalue)
           self.stop()

    def left(self,secondvalue=0):
        self.setup()
        GPIO.output(self.RightAhead_pin,GPIO.HIGH)
        if secondvalue!=0:
           time.sleep(secondvalue)
           self.stop()

    def right(self,secondvalue=0):
        self.setup()
        GPIO.output(self.LeftAhead_pin,GPIO.HIGH)
        if secondvalue!=0:
           time.sleep(secondvalue)
           self.stop()

    def rear(self,secondvalue=0):
        self.setup()
        GPIO.output(self.RightBack_pin,GPIO.HIGH)
        GPIO.output(self.LeftBack_pin,GPIO.HIGH)
        if secondvalue!=0:
           time.sleep(secondvalue)
           self.stop()

    def stop(self):
        for pin in self.inx_pin:
            GPIO.output(pin,GPIO.LOW)

程序中默认了使能脚连接5、6、13、19号引脚,控制脚连接21、22、23、24号引脚(使能脚上是由跳线帽插着的,连接树莓派时拔掉,这样会有四个引脚,这里把四个脚都连到树莓派上了,其实只需要两个就够了);对于前进、后退、左转和右转方法,可以传入一个时间值,指示运动多长时间。

6 主程序

调试好测距和电机控制,把所有东西组装好,下图是我完整的小车展示,用充电宝为树莓派供电,多个干电池为电机供电,此外我这里还把摄像头接了上去,虽然暂时没有用到。

下面需要编写一个主程序来启动小车,我的目的是简单的避障,因此可以考虑让小车直行并不停地检测前方障碍距离,当靠近障碍物时右转一定角度继续前进,下图是主程序的简单流程。

根据设计的流程,编写主程序:

#! /usr/bin/python3
import RPi.GPIO as GPIO
import sys
import time
import Ultrasonic_Module as Ul
import Motor_Module as Mo

### 模块初始化
ultrasonic=Ul.Ultrasonic_Module(25,26)
motor=Mo.Motor_Module()
print("#########initing the ultrasonic module...##########")
if not ultrasonic.setup():
    print("ultrasonic setup fall , programe stop")
    exit()
print("#########initing the motor module...##########")
motor.setup()

### 一开始小车处于前进状态
print("the car start running...") 
motor.ahead()

### 循环判断距离
distance=10
limited_d=0.05 #障碍最小距离
time_rear=0.8  #后退时长
time_right=1.2  #右转时长
try:
   while True:
      distance=ultrasonic.getdistance()
      if distance <= limited_d:
          print("run into blank wall")
          motor.stop()
          motor.rear(time_rear)
          motor.right(time_right)
          motor.ahead()
except KeyboardInterrupt:
    motor.stop()
    print("")
    print("stop the car")

程序中加入一个键盘中断处理,是为了在按下ctrl+c终止程序时及时停止小车。

7 测试

通过ssh连接小车上的树莓派,执行上面的主程序文件启动程序,运行小车。测试结果良好。由于只有一个位于车头的传感器,如果小车侧边撞上障碍物就卡在那了;此外影响小车运行的最大因素是电池,四个马达的负载有点大,干电池组很快就欠压了,所以最好还是得买锂电池。