一、作品簡介
感謝Digikey提供的這次汽車應用創(chuàng)意大賽的平臺,本項目基于樹莓派,從功能模塊上劃分一共以下四個部分:
1、倒車雷達方案對比:倒車雷達的核心原理是距離檢測,因此我計劃對比超聲波測距方案和激光測距方案的優(yōu)劣,并根據(jù)他們自身的特長將兩者結合起來完成一個間距視野角度與精度的倒車雷達。
2、車窗智能防結冰:冬天北方車輛車窗會在夜晚結冰,導致第二天早上需要相當長的時間化冰,否則將嚴重影響視線無法駕駛。車窗起霧的原因主要是車內外溫差較大,導致車內濕氣在車窗上凝結,隨后被車外的低溫凍住結冰。我計劃的解決方案是,當熄火并鎖車后,檢測車內外溫度差,若溫度差大于設定值,則開啟通風功能快速平衡車內外溫度;當溫差小于閾值后關閉通風節(jié)省電量。
3、可視無線倒車雷達:給貨車或一些其他大型車輛添加倒車雷達,走線會比較麻煩,長期還容易出現(xiàn)老化問題,因此我計劃將倒車攝像頭改為無線wifi攝像頭,將畫面通過無線傳輸實時顯示在LCD屏幕上。這樣可以大大簡化安裝難度,只需要從尾燈取電即可。
4、后視鏡智能調整:這個功能目前僅在一些頂配車型中有,我打算把這個功能也一并實現(xiàn),方便對現(xiàn)有中低配車輛進行升級。后視鏡的角度控制本質上是一個二軸舵機云臺的控制。我計劃預設三個角度,一是行車時后視鏡角度;二是倒車時后視鏡角度;三是停車時后視鏡角度。三種狀態(tài)通過檢測汽車檔位進行切換。
二、項目實物圖
三、各部分功能說明
1、倒車雷達方案對比:
倒車雷達的核心原理是距離檢測,距離檢測目前常用的有兩種方案,一種是通過激光進行檢測,原理上可分為TOF,也就是記錄發(fā)射激光和收到激光之間的時間間隔;第二是三角測距法,利用固定的激光發(fā)射角度,看反射回來激光的落點落在CCD傳感器的位置來進行測距。無論基于哪種原理,激光測距技術共同的優(yōu)點都是精度比較高,對被測物體尺寸,形狀要求小。但缺點也很明顯,激光測距視場非常窄,只能測一條線上的障礙物,如果障礙物并不在激光頭的正前方,則無法測量。
另一種方案是超聲波測距方案,也就是我們熟知的聲納。原理上和上面提到的TOF一致,都是測量發(fā)射信號和接收到反射信號之間的時間,以此來計算速度。但超聲波由于頻率較低,速度較慢,在不同介質中傳播速度不一致,同時衍射現(xiàn)象較為明顯。這些特性導致超聲波測距的精度并不高,但優(yōu)勢是視場較寬,可以檢測更大的范圍。
從上面的介紹可以看出,兩種方案的優(yōu)缺點正好可以相互互補,因此我會結合兩種方案,在實際中驗證上述差異,并結合他們的優(yōu)點,制作一個高精度雷達。
首先先說說激光雷達的使用方法。項目中我使用的激光雷達是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觸發(fā)方式來進行通訊。
首先安裝庫文件:
pip3 install adafruit-circuitpython-hcsr04 adafruit-circuitpython-us100
接下來是接線:
TRIG: BCM22
ECHO: BCM27
VCC: 3.3V
GND: GND
接著在我們剛才創(chuàng)建的文件中加入以下內容:
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等庫均出現(xiàn)問題。雖然部分功能有時還可以運行,但會有各種問題出現(xiàn),因此盡量避免使用。這里我們需要修改一下庫中adafruit_hcsr04.py的源碼,注釋以下代碼:
# try:
# frompulseio import PulseIn
# _USE_PULSEIO = True
# except (ImportError, NotImplementedError):
# pass # This is OK, we'll try tobitbang it!
接著我們測試一下雷達特性:
可以看到在距離較短時,激光雷達所得測量值更精準,也更小。在此時參考激光雷達的讀數(shù)會更為保守。
而當障礙物不在雷達正前方時,我們可以看到聲納依舊可以檢測到物體,但激光雷達檢測不到,顯示的距離遠大于聲納讀數(shù)。
因此我們可以結合上面兩個結果,只要其中任意讀數(shù)低于100,就響起警報。警報使用的是GPIO控制有源蜂鳴器實現(xiàn)。這里需要注意的是,蜂鳴器不可直接用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、車窗智能防結冰:
冬天北方車輛車窗會在夜晚結冰,導致第二天早上需要相當長的時間化冰,否則將嚴重影響視線無法駕駛。車窗起霧的原因主要是車內外溫差較大,導致車內濕氣在車窗上凝結,隨后被車外的低溫凍住結冰。我計劃的解
決方案是,當熄火并鎖車后,檢測車內外溫度差,若溫度差大于設定值,則開啟通風功能快速平衡車內外溫度;當溫差小于閾值后關閉通風節(jié)省電量。
這里面就涉及到了兩個溫度傳感器的數(shù)值讀取。為了盡可能體驗開發(fā)板功能,在這里我選取了兩個不同的傳感器。我們分來講他們的驅動方法。
首先是BMP280,這本是一顆氣壓傳感器,但是傳感器內部有溫度傳感器,用來做補償計算使用。溫度傳感器的讀數(shù)可以直接讀取,因此可以用作我們當前的應用。
我們先要在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
接著在我們剛才創(chuàng)建的文件中加入以下內容:
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運行一下,如果一切順利,應該就可以看到一下輸出:
我們可以看到兩個傳感器讀數(shù)相差無幾。
我們用手指貼在SHT40傳感器背面的PCB上,10秒后,我們就可以看到出現(xiàn)了明顯的溫度差異:
因此,假設我們把SHT40放置于車內,BMP280放置于車外,當車內溫度高與車外一定溫度時(正常這個溫度應大于10度,且同時室外溫度低于0度。但為了方便演示,這里設置為2.5度),開啟換氣風扇(風扇使用蜂鳴器進行指示),平衡車內外溫度,就可以避免車窗上出現(xiàn)冷凝水再結冰的問題。
蜂鳴器接線:
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、可視無線倒車雷達:
在給貨車或一些其他大型車輛添加倒車雷達,走線會比較麻煩,長期還容易出現(xiàn)老化問題,因此我計劃將倒車攝像頭改為無線wifi攝像頭,將畫面通過無線傳輸實時顯示在LCD屏幕上。這樣可以大大簡化安裝難度,只需要從尾燈取電即可。wifi可以用車載的AP,也可以讓樹莓派自己進入AP模式,讓無線攝像頭進行連接。
先講講無線攝像頭的部分。由于我們希望得到無線視頻流,因此只要是可以聯(lián)網的ip攝像頭在本項目中都可以應用。那么里我們就使用一種成本比較低的方案,用ESP32CAM進行實現(xiàn)。
開發(fā)板上燒錄的是arduino ide上的官方CameraWebServer例程。首先我們需要定義我們使用的開發(fā)板:
#define CAMERA_MODEL_AI_THINKER
其次是要更改wifi連接信息,也就是以下兩行:
const char* ssid = "REPLACE_WITH_YOUR_SSID";const char* password = "REPLACE_WITH_YOUR_PASSWORD";
上傳代碼后,如果一切正常,我們可以在串口監(jiān)視器里看到開發(fā)板的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、后視鏡智能調整:
這個功能目前僅在一些頂配車型中有,我打算把這個功能也一并實現(xiàn),方便對現(xiàn)有中低配車輛進行升級。后視鏡的角度控制本質上是對舵機云臺的控制。我準備預設三個角度,一是行車時后視鏡角度;二是倒車時后視鏡角度;三是停車時后視鏡角度。三種狀態(tài)通過檢測汽車檔位進行切換。
舵機驅動本應使用pwmio庫進行,但是由于樹莓派5硬件的變化,過去的RPI.GPRO庫已無法使用,而大量的庫都是建立在RPI.GPIO上的,包括pwmio。因此這里我使用digitalio自行編寫了一個pwm發(fā)生器,可以產生10個周期為50hz,可設置高電平us的pwm方波,用來給舵機發(fā)送信號。舵機接線方法如下:
PWM: BCM4
VCC: 5V
GND: GND
完成連接后,我們通過以下代碼,就可以實現(xiàn)用脈沖時間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)
現(xiàn)在我們需要一個按鍵接入來模擬檔位變化。按照真實的情景,這里應該使用的是一個多段開關,直接接入GPIO,通過GPIO電平變化來進行判斷。但GPIO的開關輸入實現(xiàn)在之前已經做過。為了避免重復,也是盡可能體驗更多的功能,在這我用了一顆電位器來模擬旋鈕換擋。
樹莓派本身并不包含ADC,因此這里我們需要外接個ADC模塊來完成模擬量采集,并轉化為數(shù)字信號傳遞給樹莓派。我選用的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進行讀?。?/p>
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讀數(shù)。轉動電位器,可以看到讀數(shù)發(fā)生相應變化。
最后,就是把所有的功能整合在一起,完成項目了。
整合方法一般是把每一個單獨的功能封裝成一個函數(shù),或是一個類,然后導入主函數(shù)中,在需要時進行調用。而在這里,由于我們的python代碼運行在一個完整的linux系統(tǒng)上,為了盡可能放大操作系統(tǒng)的優(yōu)勢,同時盡可能減小代碼直接的耦合,我們使用另一種方式進行功能整合:直接使用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來運行的。如果有特殊需求,比如要使用虛擬環(huán)境的python,那么只需要在調用時添加python變量,把虛擬環(huán)境bin中的python絕對路徑以字符串形式傳遞給該變量即可。
終止的方法是通過文件名查詢所有當前進程,獲得pid之后再殺進程實現(xiàn)的。
下面寫我們的主程序。主程序邏輯比較清晰,根據(jù)擋位不同,也就是電位器所在范圍的不同,分為三種情況:
行車擋:將后視鏡(舵機)調整為行車時角度,并關閉其他模塊;
倒車擋:將后視鏡(舵機)調整為倒車時角度,開啟倒車雷達,開啟無線倒車鏡,并關閉其他模塊;
駐車擋:將后視鏡(舵機)調整為駐車時角度,開啟防結冰結霜功能,并關閉其他模塊。
由于我是使用python文件調用其他python文件,這里有一點就要特別注意了,當我終止主程序時,由于其他運行中的python文件是在操作系統(tǒng)內被調用的,所以并不會被一同終止。因此,我們還需要再主程序中增加終止主程序時需要終止所有可調用模塊的功能。實現(xiàn)代碼如下:
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)