Android 代码已上传至Github:链接


更新(19/7/15)

  • 修改转向方案:此前左转采用左轮停转、右转正转方案实现,其优点是实现简单、复杂度低,此次更新采用pwm方案,左右均正转,单侧轮使用pwm调低转速。
self.pi_left = pigpio.pi()
self.pi_right = pigpio.pi()

def set_pwm(self, pwm, pin):
        # pwm.start(percent)
        pwm.set_PWM_frequency(pin, self.hz)
        pwm.set_PWM_dutycycle(pin, self.percent_up)
        pwm.set_PWM_range(pin, self.percent_down)
        
def stop_pwm(self, pwm, pin):
        pwm.set_PWM_dutycycle(pin, 0)
  • 采用单例模式:因为始终只有一个控制实例,使用单例模式可以节约资源、保证线程安全。
def __new__(cls, *args, **kwargs):
        if not hasattr(cls, 'instance'):
            cls.instance = super(GPIOer, cls).__new__(cls)
        print(cls.instance)
        return cls.instance

准备工作

  • 小车底座
  • 5v树莓派供电
  • 12v电机驱动电源 (我用的是双输出口的充电宝 一个给pi 一个驱动)
  • 四个电机
  • l298n驱动一个
  • 树莓派3b一只
  • 三种杜邦线各20根
  • 红外、超声波、小灯等模块(可选)
  • 树莓派摄像头一枚

线路连接

由于一个l298n驱动只能控制两个电机,但是想做成四轮小车于是采用两侧的电机并联,即两侧的电机同步转动。树莓派和驱动的接线按照下图(这个图是3b的,其他型号pi的gpio针脚会略有不同)需要注意的一点是图上那个EN_A和EN_B那个是需要拔掉那个短接帽.


程序测试


从真值表可以看出给IN1 IN2和EN通不同的电平即可产生不同的效果。

#采用BCM编码
self.left_en = 14
self.left_in1 = 15
self.left_in2 = 18
self.right_en = 25
self.right_in3 = 8
self.right_in4 = 7

def up():
    dict_up = {self.right_en: True, self.right_in3: True, self.right_in4: False,self.left_en: True,self.left_in1: True, self.left_in2: False}
    self.setGpioout(dict_up)
    self.stop_pwm(self.pi_left, self.left_en)
    self.stop_pwm(self.pi_right, self.right_en) 
    
def setGpioout(self, dicts):
    GPIO.setmode(GPIO.BCM)
    for key in dicts.keys():
       GPIO.output(key, dicts[key]) 
       
def stop_pwm(self, pwm, pin):
    pwm.set_PWM_dutycycle(pin, 0)

红外避障模块试用

这款模块只有3个接口 VCC可以接3.3或者5v GND接地 OUT接GPIO,由于需要在左右两侧都考虑避障,所以得最少买两个模块。红外避障模块使用起来很简单,直接读取OUT接口的数据判断即可。

red_left=16
red_right=12
GPIO.setup(red_left,GPIO.IN)
GPIO.setup(red_right,GPIO.IN)
if GPIO.input(red_left) && GPIO.input(red_right):
	t_up()


超声波模块试用

我买的超声波模块型号是HY-SRF05,接线方案为:ucc接5v,trig接23,echo接24,gnd接23紧挨着的gnd。还有需要注意的一点是,超声波模块似乎需要方向,即当倒置模块时测距误差会变得特别大。

import RPi.GPIO as GPIO
import time
Trig_Pin=23
Echo_Pin=24
def init_hy():
	GPIO.setup(Trig_Pin, GPIO.OUT, initial = GPIO.LOW)
	GPIO.setup(Echo_Pin, GPIO.IN)

def checkdist():
	GPIO.output(Trig_Pin, GPIO.HIGH)
    time.sleep(0.00015)
    GPIO.output(Trig_Pin, GPIO.LOW)
    while not GPIO.input(Echo_Pin):
        pass
    t1 = time.time()
    while GPIO.input(Echo_Pin):
        pass
    t2 = time.time()
    return (t2-t1)*340*100/2

if __name__ == '__main__':
	try:
   		while True:
        print('Distance:'+checkdist()+'cm')
        time.sleep(1)
	except KeyboardInterrupt:
    	GPIO.cleanup()


摄像头使用

ssh连接到pi(使用非root用户登录),输入指令sudo raspivid -o - -t 0 -w 640 -h 360 -fps 25|cvlc -vvv stream:///dev/stdin --sout '#standard {access=http,mux=ts,dst=:8080}' :demux=h264 接着用vlc的串流播放地址:http://你的树莓派ip:8080,这个方案相比之前用 mjpg-streamer 而言,会有3秒的延迟。


呼吸灯

小灯这个只需要两个GPIO口,一个接3.3v,另一个我接了18

import RPi.GPIO
import time

RPi.GPIO.setmode(RPi.GPIO.BCM)
RPi.GPIO.setup(18, RPi.GPIO.OUT)
pwm = RPi.GPIO.PWM(18, 50)
pwm.start(0)
try:
    while True:
        for i in range(0, 101, 1):
            pwm.ChangeDutyCycle(i)
            time.sleep(.01)
        for i in range(100, -1, -1):
            pwm.ChangeDutyCycle(i)
            time.sleep(.01)
except KeyboardInterrupt:
    pass
pwm.stop()
RPi.GPIO.cleanup()

操控

操控采用的是树莓派作为一个socket服务器,然后通过android客户端向树莓派发送指令,树莓派接受到指令后执行任务。 android客户端中轨迹球的部分是fork一个在github上看到的仓库,其中视频流采用的是之前用的mjpg-stream。

  • socket服务器端
import socketserver
import gpio
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            data = (self.request.recv(1024).decode('utf-8'))
            if not data:
                break
            data=data.replace('\n','').replace(' ','')
            response = gpio.ctrl_id(data)+"\n"
            self.request.sendall(response.encode('utf-8'))
if __name__ == "__main__":
    # Port 0 means to select an arbitrary unused port
    HOST, PORT = "0.0.0.0", 20000
    server = socketserver.TCPServer((HOST, PORT), ThreadedTCPRequestHandler)
    server.serve_forever()  

代码里面有换行符的修改是因为android在测试的时候发现java的socket发送会自动以换行符结尾,同样读取也是以换行符为终点。此外发现每次java的发送过程中会首先发一个空的数据包,暂时还不清楚这个是为啥。

  • android socket client
// 获取 Client 端的输出/输入流
PrintWriter out = null;
try {
    out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")), true);
} catch (IOException e) {
    e.printStackTrace();
}
// 填充信息
assert out != null;
out.println(info);
BufferedReader br;
try {
    br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    msg = br.readLine();
} catch (IOException e) {
    e.printStackTrace();
}  


大功告成

剩下的就是完善整个小车,包括整合超声波避障、转向灯、摄像头等模块以及可视化操控。最后附上完工照。