一、作品简介
感谢Digikey提供的这次汽车应用创意大赛的平台,本项目基于树莓派,从功能模块上划分一共以下四个部分:
1、倒车雷达方案对比:倒车雷达的核心原理是距离检测,因此我计划对比超声波测距方案和激光测距方案的优劣,并根据他们自身的特长将两者结合起来完成一个间距视野角度与精度的倒车雷达。
2、车窗智能防结冰:冬天北方车辆车窗会在夜晚结冰,导致第二天早上需要相当长的时间化冰,否则将严重影响视线无法驾驶。车窗起雾的原因主要是车内外温差较大,导致车内湿气在车窗上凝结,随后被车外的低温冻住结冰。我计划的解决方案是,当熄火并锁车后,检测车内外温度差,若温度差大于设定值,则开启通风功能快速平衡车内外温度;当温差小于阈值后关闭通风节省电量。
3、可视无线倒车雷达:给货车或一些其他大型车辆添加倒车雷达,走线会比较麻烦,长期还容易出现老化问题,因此我计划将倒车摄像头改为无线wifi摄像头,将画面通过无线传输实时显示在LCD屏幕上。这样可以大大简化安装难度,只需要从尾灯取电即可。
4、后视镜智能调整:这个功能目前仅在一些顶配车型中有,我打算把这个功能也一并实现,方便对现有中低配车辆进行升级。后视镜的角度控制本质上是一个二轴舵机云台的控制。我计划预设三个角度,一是行车时后视镜角度;二是倒车时后视镜角度;三是停车时后视镜角度。三种状态通过检测汽车档位进行切换。
二、项目实物图
三、各部分功能说明
1、倒车雷达方案对比:
倒车雷达的核心原理是距离检测,距离检测目前常用的有两种方案,一种是通过激光进行检测,原理上可分为TOF,也就是记录发射激光和收到激光之间的时间间隔;第二是三角测距法,利用固定的激光发射角度,看反射回来激光的落点落在CCD传感器的位置来进行测距。无论基于哪种原理,激光测距技术共同的优点都是精度比较高,对被测物体尺寸,形状要求小。但缺点也很明显,激光测距视场非常窄,只能测一条线上的障碍物,如果障碍物并不在激光头的正前方,则无法测量。
另一种方案是超声波测距方案,也就是我们熟知的声纳。原理上和上面提到的TOF一致,都是测量发射信号和接收到反射信号之间的时间,以此来计算速度。但超声波由于频率较低,速度较慢,在不同介质中传播速度不一致,同时衍射现象较为明显。这些特性导致超声波测距的精度并不高,但优势是视场较宽,可以检测更大的范围。
从上面的介绍可以看出,两种方案的优缺点正好可以相互互补,因此我会结合两种方案,在实际中验证上述差异,并结合他们的优点,制作一个高精度雷达。
首先先说说激光雷达的使用方法。项目中我使用的激光雷达是VL53L0X。
我们先要在python中安装必要的驱动库:
pip3 install adafruit-circuitpython-vl53l0x
安装完成后,将VL53L0X连接到树莓派的I2C接口上,接线方式如下:
SCL: BCM3
SDA: BCM2
VCC: 3.3V
GND: GND
完成连线后,新建一个python文件,写入以下代码:
import board
import time
import busio
import adafruit_vl53l0x
i2c = busio.I2C(board.SCL,board.SDA)
sensor = adafruit_vl53l0x.VL53L0X(i2c)
lazer_val = sensor.range
print("Lazer:{0} mm".format(lazer_val))
然后使用python来运行它,如果一切顺利,应该就可以看到一下输出
下面我们再试一下超声波传感器。市面上常见的超声波传感器有SR-04和US-100两种。其中US-100支持串口输出,温度测量,精度范围也更好,但实际体验下来差别不大。因此这里我们使用最经典的TRIG-ECHO触发方式来进行通讯。
首先安装库文件:
pip3 install adafruit-circuitpython-hcsr04 adafruit-circuitpython-us100
接下来是接线:
TRIG: BCM22
ECHO: BCM27
VCC: 3.3V
GND: GND
接着在我们刚才创建的文件中加入以下内容:
import adafruit_hcsr04
sonar = adafruit_hcsr04.HCSR04(trigger_pin=board.D22, echo_pin=board.D27)
sonar_val = int(sonar.distance*10)
print("Sonar:{0} mm".format(sonar_val))
用python来运行它,如果一切顺利,应该就可以看到一下输出:
这里多说一句,由于新的树莓派5硬件变化,过去的pwmio,pulseio等库均出现问题。虽然部分功能有时还可以运行,但会有各种问题出现,因此尽量避免使用。这里我们需要修改一下库中adafruit_hcsr04.py的源码,注释以下代码:
# try:
# frompulseio import PulseIn
# _USE_PULSEIO = True
# except (ImportError, NotImplementedError):
# pass # This is OK, we'll try tobitbang it!
接着我们测试一下雷达特性:
可以看到在距离较短时,激光雷达所得测量值更精准,也更小。在此时参考激光雷达的读数会更为保守。
而当障碍物不在雷达正前方时,我们可以看到声纳依旧可以检测到物体,但激光雷达检测不到,显示的距离远大于声纳读数。
因此我们可以结合上面两个结果,只要其中任意读数低于100,就响起警报。警报使用的是GPIO控制有源蜂鸣器实现。这里需要注意的是,蜂鸣器不可直接用GPIO驱动,如果直接驱动,不但功率低声音小,还有一定损坏GPIO的风险。我们要使用三极管或mos管来驱动蜂鸣器。
接线方式如下:
5V: 5V
signal: BCM17
GND: GND
完整代码如下:
import board
import time
import busio
import adafruit_vl53l0x
i2c = busio.I2C(board.SCL,board.SDA)
print(i2c.scan())
sensor = adafruit_vl53l0x.VL53L0X(i2c)
import adafruit_hcsr04
sonar = adafruit_hcsr04.HCSR04(trigger_pin=board.D22, echo_pin=board.D27)
from digitalio import DigitalInOut, Direction, Pull
buz = DigitalInOut(board.D17)
buz.direction = Direction.OUTPUT
while True:
try:
time.sleep(0.5)
lazer_val = sensor.range
sonar_val =int(sonar.distance*10)
print("Lazer: {0} mm".format(lazer_val))
print("Sonar: {0} mm".format(sonar_val))
if lazer_val < 100 or sonar_val < 100:
buz.value = 1
else:
buz.value = 0
except:
print("Retrying!")
time.sleep(0.5)
2、车窗智能防结冰:
冬天北方车辆车窗会在夜晚结冰,导致第二天早上需要相当长的时间化冰,否则将严重影响视线无法驾驶。车窗起雾的原因主要是车内外温差较大,导致车内湿气在车窗上凝结,随后被车外的低温冻住结冰。我计划的解
决方案是,当熄火并锁车后,检测车内外温度差,若温度差大于设定值,则开启通风功能快速平衡车内外温度;当温差小于阈值后关闭通风节省电量。
这里面就涉及到了两个温度传感器的数值读取。为了尽可能体验开发板功能,在这里我选取了两个不同的传感器。我们分来讲他们的驱动方法。
首先是BMP280,这本是一颗气压传感器,但是传感器内部有温度传感器,用来做补偿计算使用。温度传感器的读数可以直接读取,因此可以用作我们当前的应用。
我们先要在python中安装必要的驱动库:
pip3 install adafruit-circuitpython-bmp280
安装完成后,将bmp280连接到树莓派的I2C接口上,接线方式如下:
SCL: BCM3
SDA: BCM2
VCC: 3.3V
GND: GND
完成连线后,新建一个python文件,写入以下代码:
import boardimport busioimport adafruit_bmp280i2c = busio.I2C(board.SCL, board.SDA)bmp280 = adafruit_bmp280.Adafruit_BMP280_I2C(i2c, address = 0x76)bmp280.sea_level_pressure = 1018.25print("Temperature: %0.1f C" % bmp280.temperature)print("Pressure: %0.1f hPa" % bmp280.pressure)print("Altitude = %0.2f meters" % bmp280.altitude)
然后使用python来运行它,如果一切顺利,应该就可以看到一下输出:
接下来是另一个传感器,我使用的是SHT48。这是一款温湿度传感器,体积非常小,同样使用I2C进行通讯。
首先安装库文件:
pip3 install adafruit-circuitpython-sht4x
接线和上面一样:
SCL: BCM3
SDA: BCM2
VCC: 3.3V
GND: GND
接着在我们刚才创建的文件中加入以下内容:
import adafruit_sht4xsht = adafruit_sht4x.SHT4x(i2c)print("") print("Found SHT4x with serial number", hex(sht.serial_number))sht.mode = adafruit_sht4x.Mode.NOHEAT_HIGHPRECISIONprint("Current mode is: ", adafruit_sht4x.Mode.string[sht.mode])temperature, relative_humidity = sht.measurementsprint("Temperature: %0.1f C" % temperature)print("Humidity: %0.1f %%" % relative_humidity)print("")
用python运行一下,如果一切顺利,应该就可以看到一下输出:
我们可以看到两个传感器读数相差无几。
我们用手指贴在SHT40传感器背面的PCB上,10秒后,我们就可以看到出现了明显的温度差异:
因此,假设我们把SHT40放置于车内,BMP280放置于车外,当车内温度高与车外一定温度时(正常这个温度应大于10度,且同时室外温度低于0度。但为了方便演示,这里设置为2.5度),开启换气风扇(风扇使用蜂鸣器进行指示),平衡车内外温度,就可以避免车窗上出现冷凝水再结冰的问题。
蜂鸣器接线:
5V: 5V
signal: BCM17
GND: GND
完整代码如下:
import timeimport boardimport busioimport adafruit_bmp280i2c = busio.I2C(board.SCL, board.SDA)print(i2c.scan())bmp280 = adafruit_bmp280.Adafruit_BMP280_I2C(i2c, address = 0x76)bmp280.sea_level_pressure = 1013.25 import adafruit_sht4xsht = adafruit_sht4x.SHT4x(i2c)print("Found SHT4x with serial number", hex(sht.serial_number))sht.mode = adafruit_sht4x.Mode.NOHEAT_HIGHPRECISION# Can also set the mode to enable heater# sht.mode = adafruit_sht4x.Mode.LOWHEAT_100MSprint("Current mode is: ", adafruit_sht4x.Mode.string[sht.mode]) from digitalio import DigitalInOut, Direction, Pullled = DigitalInOut(board.D17) #定义引脚编号led.direction = Direction.OUTPUT #IO为输出 while True: # print("Temperature: %0.1f C" % bmp280.temperature) # print("Pressure: %0.1f hPa" % bmp280.pressure) # print("Altitude = %0.2f meters" % bmp280.altitude) # print("") # temperature, relative_humidity = sht.measurements # print("Temperature: %0.1f C" % temperature) # print("Humidity: %0.1f %%" % relative_humidity) # print("") outer = bmp280.temperature inner = sht.measurements[0 print("outer: %0.1f C" % outer) print("inner: %0.1f C" % inner) print("") if (inner - outer > 2.5): led.value = 1 print(1) else: led.value = 0 print(0) time.sleep(0.5)
3、可视无线倒车雷达:
在给货车或一些其他大型车辆添加倒车雷达,走线会比较麻烦,长期还容易出现老化问题,因此我计划将倒车摄像头改为无线wifi摄像头,将画面通过无线传输实时显示在LCD屏幕上。这样可以大大简化安装难度,只需要从尾灯取电即可。wifi可以用车载的AP,也可以让树莓派自己进入AP模式,让无线摄像头进行连接。
先讲讲无线摄像头的部分。由于我们希望得到无线视频流,因此只要是可以联网的ip摄像头在本项目中都可以应用。那么里我们就使用一种成本比较低的方案,用ESP32CAM进行实现。
开发板上烧录的是arduino ide上的官方CameraWebServer例程。首先我们需要定义我们使用的开发板:
#define CAMERA_MODEL_AI_THINKER
其次是要更改wifi连接信息,也就是以下两行:
const char* ssid = "REPLACE_WITH_YOUR_SSID";const char* password = "REPLACE_WITH_YOUR_PASSWORD";
上传代码后,如果一切正常,我们可以在串口监视器里看到开发板的ip。
下面我们回到树莓派。首先安装需要的库:
pip3 install opencv-python
接着新建python文件,写入以下代码:
import cv2 url = "http://192.168.1.91:81/stream"cap = cv2.VideoCapture(url) while (True): ret, frame = cap.read() cv2.imshow('frame', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release()cv2.destroyAllWindows()
注意由于这里需要可视化的界面,所以我们不能再像之前那样在ssh中操作。我们需要接上显示器进入桌面,然后在桌面打开终端,在终端里用python运行上面的程序,运行后,我们就可以看到已经成功从无线摄像头中获取到图像。
4、后视镜智能调整:
这个功能目前仅在一些顶配车型中有,我打算把这个功能也一并实现,方便对现有中低配车辆进行升级。后视镜的角度控制本质上是对舵机云台的控制。我准备预设三个角度,一是行车时后视镜角度;二是倒车时后视镜角度;三是停车时后视镜角度。三种状态通过检测汽车档位进行切换。
舵机驱动本应使用pwmio库进行,但是由于树莓派5硬件的变化,过去的RPI.GPRO库已无法使用,而大量的库都是建立在RPI.GPIO上的,包括pwmio。因此这里我使用digitalio自行编写了一个pwm发生器,可以产生10个周期为50hz,可设置高电平us的pwm方波,用来给舵机发送信号。舵机接线方法如下:
PWM: BCM4
VCC: 5V
GND: GND
完成连接后,我们通过以下代码,就可以实现用脉冲时间us的方式控制舵机,并设置舵机居中,即us=1500:
importboard
importtime
fromdigitalio import DigitalInOut, Direction, Pull
pwm_pin= DigitalInOut(board.D4)
pwm_pin.direction= Direction.OUTPUT
defto(ns, freq = 50):
cycle = 1 * 1_000_000 / freq
for i in range(10):
cycle_begin = time.time() * 1000_000
pwm_pin.value = 1
while (time.time() * 1000_000 - cycle_begin) < ns:
pass
pwm_pin.value = 0
while (time.time() * 1000_000 - cycle_begin) < cycle:
pass
to(1500)
现在我们需要一个按键接入来模拟档位变化。按照真实的情景,这里应该使用的是一个多段开关,直接接入GPIO,通过GPIO电平变化来进行判断。但GPIO的开关输入实现在之前已经做过。为了避免重复,也是尽可能体验更多的功能,在这我用了一颗电位器来模拟旋钮换挡。
树莓派本身并不包含ADC,因此这里我们需要外接个ADC模块来完成模拟量采集,并转化为数字信号传递给树莓派。我选用的ADC模块是ADS1115,这一一块16bits的ADC,输出十进制的范围是0-65535,精度比较高。
接线方法如下:
SCL: BCM3
SDA: BCM2
VCC: 3.3V
GND: GND
A0: 电位器中间
电位器两端一端接3.3V,一端接GND。
先安装对应的库:
pip3 install adafruit-circuitpython-ads1x15
随后通过下面代码初始化ADC。我把电位器接在了A0,因此代码中使用P0进行读取:
import busioimport boardimport adafruit_ads1x15.ads1115 as ADSfrom adafruit_ads1x15.analog_in import AnalogIni2c = busio.I2C(board.SCL, board.SDA)ads = ADS.ADS1115(i2c)pot = AnalogIn(ads, ADS.P0)while 1: print(pot.value)
运行以上代码,可以看到终端中不断打印出ADC读数。转动电位器,可以看到读数发生相应变化。
最后,就是把所有的功能整合在一起,完成项目了。
整合方法一般是把每一个单独的功能封装成一个函数,或是一个类,然后导入主函数中,在需要时进行调用。而在这里,由于我们的python代码运行在一个完整的linux系统上,为了尽可能放大操作系统的优势,同时尽可能减小代码直接的耦合,我们使用另一种方式进行功能整合:直接使用python来运行/停止其他程序。
在这里我们需要预先定义运行和停止的方法,我们新建一个control.py文件,把控制代码写在里面:
import os, signal def kill(filename): cmd_run="ps aux | grep {}".format(filename) pipe=os.popen(cmd_run) for line in pipe.read().splitlines(): # print(line) if "python" in line: pid = int(line.split()[1]) a = os.kill(pid,signal.SIGKILL) # print("已杀死pid为%s的进程, 返回值是:%s" % (pid, a)) pipe.close() def run(filename, python = "python"): os.system(python + " ./" + filename + " &") # 在运行命令后面加上&符号,以另一个线程跑
从以上代码中可以看出,运行代码默认是通过python来运行的。如果有特殊需求,比如要使用虚拟环境的python,那么只需要在调用时添加python变量,把虚拟环境bin中的python绝对路径以字符串形式传递给该变量即可。
终止的方法是通过文件名查询所有当前进程,获得pid之后再杀进程实现的。
下面写我们的主程序。主程序逻辑比较清晰,根据挡位不同,也就是电位器所在范围的不同,分为三种情况:
行车挡:将后视镜(舵机)调整为行车时角度,并关闭其他模块;
倒车挡:将后视镜(舵机)调整为倒车时角度,开启倒车雷达,开启无线倒车镜,并关闭其他模块;
驻车挡:将后视镜(舵机)调整为驻车时角度,开启防结冰结霜功能,并关闭其他模块。
由于我是使用python文件调用其他python文件,这里有一点就要特别注意了,当我终止主程序时,由于其他运行中的python文件是在操作系统内被调用的,所以并不会被一同终止。因此,我们还需要再主程序中增加终止主程序时需要终止所有可调用模块的功能。实现代码如下:
import controldef handler(signal, frame): control.kill("distance.py") control.kill("temp.py") control.kill("cam.py") exit() signal.signal(signal.SIGTSTP, handler) # Ctrl+Zsignal.signal(signal.SIGINT, handler) # Ctrl+C
至此,所有功能都已完成。完整的代码如下:
import timeimport boardimport servo import busioimport adafruit_ads1x15.ads1115 as ADSfrom adafruit_ads1x15.analog_in import AnalogIni2c = busio.I2C(board.SCL, board.SDA)ads = ADS.ADS1115(i2c)pot = AnalogIn(ads, ADS.P0) import controldef handler(signal, frame): control.kill("distance.py") control.kill("temp.py") control.kill("cam.py") exit() import signalsignal.signal(signal.SIGTSTP, handler) # Ctrl+Zsignal.signal(signal.SIGINT, handler) # Ctrl+C state = 0old_state = 0while True: if(pot.value < 1000): state = 1 elif(pot.value > 2000): state = 3 elif(1100 < pot.value < 1900): state = 2 if state is not old_state: old_state = state if state == 2: servo.to(1500) control.run("cam.py") control.run("distance.py", python = "sudo python") control.kill("temp.py") if state == 3: servo.to(2000) control.kill("cam.py") control.kill("distance.py") control.kill("temp.py") if state == 1: servo.to(1000) control.run("temp.py", python = "sudo python") control.kill("cam.py") control.kill("distance.py") time.sleep(0.1) print(pot.value)