Arduino 超音波距離檢測 效率改進

目的

由於HC-SR04超音波模組是利用聲音反射來計算距離,在物體距離較遠時ECHO Pin 會回傳一個較長的訊號,Arduino等待訊號電位變化時需要較長的時間,這會導致執行效率變差,本篇希望能改善超音波距離檢測之效率。

問題

根據HC-SR04的官方文件來讀取ECHO Pin的電位變化就能取得超音波資訊,我們可以利用 pulseIn() 函數來取得回傳訊號高電位的時間。

根據Arduino官方文件指出 pulseIn() 有三個參數 pin、value、timeout ,我們可以利用第二個參數value來設定我們要計時的電位,例如LOW或HIGH;利用第三個參數timeout來設定時間限制(非必要),預設為1秒,也就是說如果HC-SR04感測不到物體時,Arduino可能會停住等待 pulseIn() 整整1秒才會繼續下一個指令。

改進

最直覺的改進方式就是將pulseIn() 的timeout 設定的小一點,例如100ms,不過這會縮小HC-SR04能夠檢測的距離,不是一個最好的方法。

最好的方法是利用「外部中斷(External Interrupts)」來取得電位變化,並且在變化時紀錄時間。所謂中斷是讓Arduino能夠暫停目前動作先去完成其他命令,而不是等待該指令完成才繼續動作,Nick Gammon’s 的文章中有一個有趣的比喻:

Interrupts let you respond to “external” events while doing something else. For example, if you are cooking dinner you may put the potatoes on to cook for 20 minutes. Rather than staring at the clock for 20 minutes you might set a timer, and then go watch TV. When the timer rings you “interrupt” your TV viewing to do something with the potatoes.

中斷是讓你能夠在處理事情時能夠回應外部事件,例如你正要煮飯,你將馬鈴薯放入鍋子20分鐘,這時候你可以設定一個20分鐘的鬧鐘然後去看電視,等到鬧鐘響的時候中斷看電視這個動作並回到廚房繼續煮飯,而不是煮飯時盯著鍋子20分鐘。

我們希望能夠透過外部中斷來解決等待ECHO回傳的問題,因此我們對ECHO Pin設定中斷,等到電位升高時開始計時,待電位降低時停止計時,利用這個時間差來反算就能得到距離。

在Arduino中我們可以用attachInterrupt() 函數來設定中斷,並且設定中斷的觸發方式與執行函數。

首先,我們先定義電位升高與降低時所執行的函數,並且在電位降低後繼續發送下一次的TRIG脈衝:

volatile int pwmValue = 0; //脈衝值
volatile unsigned long prevTime = 0; //時間

//當電位升高時執行
void rising() {
	attachInterrupt(digitalPinToInterrupt(ECHOPIN), falling, FALLING);
	prevTime = micros(); //紀錄當下時間
}

//當電位降低時執行
void falling() {
	attachInterrupt(digitalPinToInterrupt(ECHOPIN), rising, RISING);
	pwmValue = micros() - prevTime; //取得時間差

	float distance = (float)pwmValue / 29 / 2;
	Serial.print(distance);
	Serial.println("cm");
	
	ping(); //發送脈衝
}

由於並不是每個Pin腳都有支援外部中斷的功能,所以我們必須先查清楚支援的腳位,我們使用的是Arduino UNO,UNO僅支援Digital 2、Digital 3 (其他板可以參考attachInterrupt() 下方Description說明),所以我們將ECHO接腳改到D2,配置圖如下:

接著在setup() 中新增一個觸發:

	attachInterrupt(digitalPinToInterrupt(ECHOPIN), rising, RISING);
	ping(); //發送脈衝

除此之外,我們還要把ping() 中的return pulseIn() 移除,並將回傳值類型改成void:

void ping() //發送脈衝
{
	digitalWrite(TRIGPIN, LOW);
	delayMicroseconds(2);
	digitalWrite(TRIGPIN, HIGH);
	delayMicroseconds(10);
	digitalWrite(TRIGPIN, LOW);
}

由於不是每次Loop都發送脈衝,所以移除loop() 中的ping() 呼叫。
此外,為了能夠觀察是否能有效率的執行,只留文字輸出:

void loop() {

	Serial.println("Loop");

	delay(200);
}

完整的程式碼如下:


#define TRIGPIN 9
#define ECHOPIN 2

volatile int pwmValue = 0; //脈衝值
volatile unsigned long prevTime = 0; //時間

//發送脈衝
void ping()
{
	digitalWrite(TRIGPIN, LOW);
	delayMicroseconds(2);
	digitalWrite(TRIGPIN, HIGH);
	delayMicroseconds(10);
	digitalWrite(TRIGPIN, LOW);
}


void setup() {
	Serial.begin(9600);
	pinMode(TRIGPIN, OUTPUT);
	pinMode(ECHOPIN, INPUT);

	attachInterrupt(digitalPinToInterrupt(ECHOPIN), rising, RISING);

	ping(); //發送脈衝
}

void loop() {

	Serial.println("Loop");

	delay(200);
}

//當電位升高時執行
void rising() {
	attachInterrupt(digitalPinToInterrupt(ECHOPIN), falling, FALLING);
	prevTime = micros(); //紀錄當下時間
}

//當電位降低時執行
void falling() {
	attachInterrupt(digitalPinToInterrupt(ECHOPIN), rising, RISING);
	pwmValue = micros() - prevTime; //取得時間差

	float distance = (float)pwmValue / 29 / 2;
	Serial.print(distance);
	Serial.println("cm");

	ping(); //發送脈衝
}

結果

如果正確執行就會看到一堆數字噴出,物體距離越近數字噴得越快,在數字堆中可以看到穿插幾個LOOP。

由此可以發現超音波距離檢測持續在進行,當電位有變化時將會暫停主程式去執行falling() 與rising() 來計算距離,比起使用pulseIn() 效率更高。

不過有的時候會有負距離的狀況出現,例如 -150cm、-84cm等,推測是超音波雜訊(不確定),建議自行加上判斷式濾除這些不必要的資訊。

參考資料

Gammon Forum : Electronics : Microprocessors : Interrupts

Three Ways To Read A PWM Signal With Arduino

Arduino – AttachInterrupt

Ultrasonic Ranging Module HC – SR04

 


發表迴響