Arduino如何多任務(下)

2021-02-23 愛上Arduino

本篇為譯文,略有刪減,原文連結見文末。推薦理由:平時關於Arduino函數功能和各種模塊使用的教程很多,卻鮮有關於Arduino編程方法和思路的教程。而本篇文章從最基本的示例切入,深入淺出地介紹了Arduino的多任務、狀態機、面向對象以及定時器、中斷等概念,非常適合提升和開闊初學者Arduino編程的思維方式。

在本篇文章中,我們將會繼續深化上篇《Arduino如何多任務(上)》中介紹的知識點,同時了解Arduino的幾種不同中斷類型。看一看如何使用這些不同的工具和技巧,讓Arduino處理更多任務的同時仍然保持代碼的簡潔和較高的響應度。

    什麼是中斷(Interrupt)    

中斷機制可以說是現代計算機系統中的基本機制之一,不光像Arduino這樣的嵌入設備上的晶片有,複雜如PC上的CPU也有中斷機制。

那到底什麼是中斷機制呢?打個比方:

你在看一本書,突然你的手機來電話了,這時候你會先放下書,然後接電話,接完電話後你繼續拿起書接著看。

這個過程中,手機電話鈴聲就是中斷信號(Interrupt Signal)。這個信號告訴你(處理器),有急事了,你要放下手中正在做的事,先處理這個電話緊急事件,接電話這個過程就是中斷處理函數(Interrupt Handler)。

在Arduino環境裡,中斷處理函數就像普通的void函數一樣,你可以自己定義,然後綁定在特定的中斷上。當這個中斷信號被觸發時,這個中斷處理函數就會被調用。中斷處理函數執行完畢後,處理器會繼續處理中斷前的事務。

    中斷信號如何產生? 

有以下幾種方式可以產生中斷:

Arduino晶片內部的定時器Timer

外部中斷針腳狀態變化引起的外部中斷(External Interrupts),UNO外部中斷針腳為D2、D3

針腳組狀態變化引起針腳變化中斷(Pin-change Interrupts),UNO針腳組有3組:A0-A5,D0-D7和D8-D13。

    使用中斷的好處    

使用中斷一個最大的好處是,你不用在每次loop()裡去條件判斷一個事件是否已經開始了。而且你再也不必擔心延遲響應,或者由於某個子片段長時間佔時導致沒有監測到按鈕按下等情況。

只要中斷條件被觸發,處理器會第一時間掛起正在處理的事務,優先處理中斷處理函數。你所要做的就是規定好中斷處理函數的具體內容。

    定時中斷/Timer Interrupts    

不用你來叫我們,到時間了我們提醒你!

在上篇中,我們介紹了如何用millis()代替delay(),可以不霸佔處理器實現控時。但這種方法也有它的問題:我們在每次loop循環裡調用一次millis()來獲取當前的時間戳,然後跟上一次的時間比較,看一看是否滿足時間要求了。其實這樣操作有點浪費資源,因為在1毫秒(millisecond)的時間裡,我們可能不止一次地調用millis(),而大部分時間裡判斷條件是不滿足的。那麼。我們可不可以不要這麼頻繁地調用millis(),每隔1毫秒檢查一次是不是更好呢?

定時器和定時中斷就是為此量身設計的。我們可以創建一個每毫秒產生中斷的定時器。這個定時器會觸發中斷處理函數來檢查時間。

Arduino定時器

Arduino UNO內部有三個定時器:Timer0,Timer1和Timer2。實際上,Arduino已經將Timer0的溢出中斷運用到了delay()、delayMicroseconds、millis()、micros()這些時間函數裡,比如每隔1毫秒,Timer0會更新millis()的計數值。而這個頻率正好是我們需要的,所以我們一會兒就直接使用Timer0來產生我們所需的中斷。

頻率和計數

定時器簡而言之就是以一定頻率更新的計數器。而個頻率的來源就是單片機的內部時鐘或者外部晶振產生的時鐘。Arduino UNO默認系統時鐘是16MHz。你可以設置不同的分頻來更改頻率和不同的計數模式。你也可以設置在特定的數值時產生中斷。

Timer0是8位計數器,從0加到255然後可以產生一個溢出中斷。因為默認預分頻器值是64,那麼:

中斷頻率(Hz) = Arduino系統時鐘 / 預分頻器*(計數器周期+1)= 16MHz/64*(255+1)=976.5625Hz

剛好接近我們需要的1KHz,所以我們也不需要額外修改Timer0的頻率,況且我們也不能亂改Timer0,不然會導致millis()、micros()這些函數錯亂。

比較匹配寄存器

Timer0除了計數溢出產生中斷(TIMER0_OVF),還可以設置比較匹配寄存器來產生中斷(TIMER0_COMPA和TIMER0_COMPB)。

比如我們可以設置名為OCR0A的寄存器,比較值設為175(十六進位為0xAF),Timer0每次計數時都會跟OCR0A的值比較,如果相等那麼就會產生一個中斷。代碼如下:

      OCR0A = 0xAF;    TIMSK0 |= _BV(OCIE0A);

然後我們需要定義中斷處理函數

SIGNAL(TIMER0_COMPA_vect){  unsigned long currentMillis = millis();  sweeper1.Update(currentMillis);  sweeper2.Update(currentMillis);    led1.Update(currentMillis);  led2.Update(currentMillis);  led3.Update(currentMillis); }

這樣上篇中控制三個LED閃爍和2個舵機掃臂的整體代碼可以改成這樣:

#include <Servo.h> 
class Flasher{ int ledPin; long OnTime; long OffTime;
int ledState; unsigned long previousMillis;
public: Flasher(int pin, long on, long off) { ledPin = pin; pinMode(ledPin, OUTPUT); OnTime = on; OffTime = off; ledState = LOW; previousMillis = 0; }
void Update(unsigned long currentMillis){ if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) { ledState = LOW; previousMillis = currentMillis; digitalWrite(ledPin, ledState); } else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) { ledState = HIGH; previousMillis = currentMillis; digitalWrite(ledPin, ledState); } }};
class Sweeper{ Servo servo; int pos; int increment; int updateInterval; unsigned long lastUpdate;
public: Sweeper(int interval) { updateInterval = interval; increment = 1; } void Attach(int pin){ servo.attach(pin); } void Detach(){ servo.detach(); } void Update(unsigned long currentMillis){ if((currentMillis - lastUpdate) > updateInterval) { lastUpdate = millis(); pos += increment; servo.write(pos); if ((pos >= 180) || (pos <= 0)) { increment = -increment; } } }}; Flasher led1(11, 123, 400);Flasher led2(12, 350, 350);Flasher led3(13, 200, 222);
Sweeper sweeper1(25);Sweeper sweeper2(35); void setup() { sweeper1.Attach(9); sweeper2.Attach(10);      OCR0A = 0xAF; TIMSK0 |= _BV(OCIE0A);} SIGNAL(TIMER0_COMPA_vect) { unsigned long currentMillis = millis(); sweeper1.Update(currentMillis); if(digitalRead(2) == HIGH) { sweeper2.Update(currentMillis); led1.Update(currentMillis); } led2.Update(currentMillis); led3.Update(currentMillis);}
void loop(){}

這樣,在loop()裡我們連一行代碼也沒寫,甚至你現在可以在loop()裡隨便用delay()也沒關係,因為現在LED閃爍和舵機控制完全用中斷實現,跟主循環已經完全解耦。

    外部中斷/External Interrupts    

與定時中斷不同,外部中斷由外部的事件觸發,比如當一個按鈕被按下、或者處理器收到編碼器產生的一個脈衝信號。但和定時中斷一樣,外部中斷同樣不需要你在主循環loop()函數裡不斷地輪詢某個針腳的狀態變化。

Arduino UNO有兩個外部中斷針腳,分別為D2、D3。

如圖連接好電路,我們將使用D2上的按鈕來重置我們的舵機擺臂。

首先,我們在Sweeper類裡添加一個名為reset()成員函數,這個函數的主要作用就是無論舵機角度在哪,立馬讓舵機轉到零度位置。

  void reset()    {        pos = 0;    servo.write(pos);    increment = abs(increment);  }

然後我們創建一個大寫名為Reset的中斷處理函數,函數內調用兩個舵機的reset()函數。

void Reset(){  sweeper1.reset();  sweeper2.reset();}

最後,我們只要通過AttachInterrupt()函數,將Reset函數綁定指定的外部中斷即可。UNO  Interrupt 0對應就是D2針腳,當D2上的按鈕被按下時,電平信號會FALLING(從HIGH高電平到LOW低電平),這樣就會觸發中斷執行Reset。

pinMode(2, INPUT_PULLUP); attachInterrupt(0, Reset, FALLING);

整體代碼如下:

#include <Servo.h>
class Flasher{ int ledPin; long OnTime; long OffTime;
volatile int ledState; volatile unsigned long previousMillis;
public: Flasher(int pin, long on, long off) { ledPin = pin; pinMode(ledPin, OUTPUT); OnTime = on; OffTime = off; ledState = LOW; previousMillis = 0; }
void Update(unsigned long currentMillis){ if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime)) { ledState = LOW; previousMillis = currentMillis; digitalWrite(ledPin, ledState); } else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime)) { ledState = HIGH; previousMillis = currentMillis; digitalWrite(ledPin, ledState); } }};
class Sweeper{ Servo servo; int updateInterval; volatile int pos; volatile unsigned long lastUpdate; volatile int increment;
public: Sweeper(int interval) { updateInterval = interval; increment = 1; } void Attach(int pin){ servo.attach(pin); } void Detach(){ servo.detach(); } void reset(){ pos = 0; servo.write(pos); increment = abs(increment); } void Update(unsigned long currentMillis){ if((currentMillis - lastUpdate) > updateInterval) { lastUpdate = currentMillis; pos += increment; servo.write(pos); if ((pos >= 180) || (pos <= 0)) { increment = -increment; } } }}; Flasher led1(11, 123, 400);Flasher led2(12, 350, 350);Flasher led3(13, 200, 222);
Sweeper sweeper1(25);Sweeper sweeper2(35); void setup() { sweeper1.Attach(9); sweeper2.Attach(10);    OCR0A = 0xAF; TIMSK0 |= _BV(OCIE0A); pinMode(2, INPUT_PULLUP); attachInterrupt(0, Reset, FALLING);} void Reset(){ sweeper1.reset(); sweeper2.reset();}
SIGNAL(TIMER0_COMPA_vect) { unsigned long currentMillis = millis(); sweeper1.Update(currentMillis); sweeper2.Update(currentMillis);   led1.Update(currentMillis); led2.Update(currentMillis); led3.Update(currentMillis);}
void loop(){}

上傳代碼到Arduino UNO,當我們按一下按鈕時,兩個舵機就是立馬就歸零。

    針腳變化中斷/Pin-change Interrupts    

Arduino UNO只有兩個外部中斷的針腳,那如果2個不夠怎麼辦?幸運地是,UNO的所有針腳都支持針腳變化中斷(Pin-change Interrupts),

但和外部中斷不同的是,針腳變化中斷是以組存在的。比如UNO有三組:A0-A5,D0-D7和D8-D13。組內任何一個針腳的狀態變化都會觸發這個組的針腳變化中斷。所以與外部中斷的單一陣腳不同,使用針腳變化中斷會稍微複雜些,因為你必須要知道一組內到底是哪個針腳的狀態變化了。

這裡我們就不展開了。

    定時器和中斷的使用忠告   

定時器和中斷雖然好用,但也不能濫用!

1. 如果一切事務都是緊急事務,那麼它們裡沒有一件是緊急的。

中斷應該被用來處理最緊急且用時比較短的事務。要記住,當處理器處於一個中斷裡時,其他的事務都是被掛起凍結的。如果該中斷處理函數佔用太多時間,其他事務和中斷就可能沒有時間執行。

2. 一次一個中斷

當處理器處於中斷處理流程(ISR, Interrupt Service Routine)時,其他中斷是處於停滯狀態的。所以要注意:

3. 將耗時運算放到loop()裡

如果,你事務實在比較長,那麼儘量讓中斷處理最重要的部分,把耗時的運算放到主循環裡,通過共享變量等方法精簡中斷處理。

4. 不要隨意修改定時器的配置

處理器裡的定時器屬於有限資源,比如Arduino UNO裡只有3個定時器,但它們被很多函數共享。如果你隨意修改,可能會牽一髮而動全身,導致其他函數錯亂。以UNO為例:

Timer0 - 用於millis(), micros(), delay()函數,還有D5、D6針腳的PWM

Timer1 - 用於Servos, WaveHC等庫,還有D9、D10針腳的PWM

Timer2 - 用於Tone庫,還有D11、D13針腳的PWM

5. 安全地共享變量

因為處理器在進入中斷後,主循環所有狀態都會被「凍」起來,直到中斷結束,所以對於中斷函數和loop()的變量交換我們要十分小心。

使用volatile變量

有時候代碼編譯的時候,編輯器為了優化程序速度會加一些優化。程序運行時,這些優化會把代碼中常用的變量在內存裡做一份拷貝用來快速訪問。所以在共享變量的時候,可能會產生變量拷貝和原值不同步的情況。所以在這種變量申明的時候,儘量加上volatile修飾符,用來告訴編輯器不要給這個變量優化,每次都要去讀取原值。

保護超長變量

當你使用比整數類型(integer)還要大的變量時,比如strings, arrays, structures,在變量前加volatile估計還不夠。超長變量一般需要一定的周期更新。可能在更新過程中,中斷就發生了,那麼這個變量可能就會被破壞。所以如果你在主循環裡要更新一個超長變量時,最好在更新前關閉相關的中斷,更新好以後再開啟。

    總結    

在本篇教程中,我們了解並學習了如何使用定時器中斷和外部中斷來「升級」我們的代碼,也談了一些定時器和中斷使用的注意事項。

定時器、中斷的知識並不是只有學習Arduino時才有用,可以說理解並掌握定時器和中斷的使用對於任何一種嵌入式平臺的進階都是必要的。結合上篇教程中介紹的狀態機、面向對象編程等概念,可以說我們已經有足夠的工具來解決絕大部分等編程問題了。

原文連結:https://learn.adafruit.com/multi-tasking-the-arduino-part-2

相關焦點

  • Arduino如何多任務(上)
    而本篇文章從最基本的示例切入,深入淺出地介紹了Arduino的多任務、狀態機、面向對象以及定時器、中斷等概念,非常適合提升和開闊初學者Arduino編程的思維方式。當Arduino初學者掌握了基本的點亮LED,簡單的一些傳感器和舵機掃動,下一步就要面對的是更大更複雜的項目。這些項目需要初學者學會「東拼西湊」,即把簡單的代碼片段拼成一個能用的整體。
  • Windows系統下Arduino IDE 的下載與安裝
    1、瀏覽器打開arduino的官方網站https://www.arduino.cc/(英文網站,文末有切換中文方式)  2、進入SOFTWARE---DOWNLOADS。 (綠色免安裝,解壓後打開arduino.exe即可使用)③:win8.1及win10系統,應用商店下載。(打開系統自帶應用商店搜索即可,此方法不做具體介紹)4、下面以下載exe版本的arduino IDE為例進行說明。點擊「Windows Installer, for Windows XP and up 」進入到一個開源軟體捐獻頁面。
  • 如何使用Visual Studio Code開發Arduino
    今天我們就介紹一下,如何用微軟的Visual Studio Code配置Arduino開發環境。1. Visual Studio Code(VSCode)是微軟公司推出的輕量級代碼編輯器,提供了豐富的插件支持,其中也包括Arduino插件。
  • Arduino之點亮LED
    您知道什麼是arduino嗎?您知道arduino可以做什麼嗎?今天讓我們認識一下arduino!arduino是一個開源的電子原型平臺,方便,靈活,方便,方便。包括硬體(各種型號的arduino板)和軟體(ArduinoIDE)。由歐洲開發團隊在冬季開發。
  • 如何為Arduino IDE安裝添加庫
    由於Arduino是一款非常流行的開源平臺,網上有大量的第三方庫供我們下載,這些庫可以幫助我們實現很多獨立難以完成的任務。 第三方庫其實就是我們從網絡或其它途徑所獲得的開源Arduino 庫。Arduino已經發展了很多年,網上總有很多熱心的編程高手大神製作好了庫並免費開放給公眾。
  • 關於Arduino技術的設計開發和應用的常見問題匯總
    Arduino最大的優勢在於開源平臺有大量的開源軟體可以調用,這些開源程序庫調用簡單,可以用少量代碼完成既定任務。但是劣勢在於封裝程度高,在進行複雜任務開發時可能遇到開源程序兼容性問題。Q:Arduino開發是否適合應用在小學編程課程內?是否可以當成早期的編程啟蒙課程?A:可以,目前已經有大量的初中生,小學生通過Scratch工具接觸、學習Arduino。
  • 詳解arduino uno製作學習
    本期將講解如何自製arduino uno。目前常見的有國產版和義大利版,兩者處理器晶片atmega328p封裝、usb轉串口下載驅動有點區別外,其他性能一樣,價格也挺大的。一般義大利版本130多rmb。國產一般售價15rmb左右現在以國產版本設計給大家講解,主要適合初學者入門學習,擁有自己的一塊arduino uno。原理圖設計還有pcb加工都採用嘉立創。成本低,溝通製作等比較齊全。
  • arduino yun雲端感受
    當然Linino還不只這些功能,若你使用Arduino 1.5.4版本的Arduino IDE,可以透過無線網路方式進行Arduino程式燒錄韌體,哇賽!這不就是不用拿著USB線跟著電腦跑嗎?沒錯!就是你想像的那樣簡單好用,以下示範如何做到這件事情。 1. 把Arduino Yun接上MicroUSB接口,不是旁邊的USB接口喔!,不要接錯了
  • 嵌入式系列 | BMP280在Arduino下的使用
    年初的時候有了個小項目的想法,是基於嵌入式的,因為很早之前玩過Arduino,所以就想著先用arduino全部跑一邊所用到的傳感器,最後在用
  • 玩轉浪漫之Arduino花式點燈
    只要稍微有一點c語言基礎和初高中物理知識,就能利用arduino創造出許多好玩的智能小物件。本篇博文將簡單介紹如何使用arduino逐步實現簡單的物聯網控制:點燈,串口點燈,利用開發板點燈。一、準備首先我們需要下載arduino的開發環境去www.arduino.cc 下載 Arduino IDE軟體。一路下一步安裝打開開發環境。代碼區域主要分為setup和loop函數:setup函數部分是指開始程序前對一些環境的測試,而loop代碼段則是執行後arduino執行的部分。
  • 如何使用arduino發送郵件
    使用的硬體:wemos D1開發板(基於ESP8266)使用的軟體:arduino IDE
  • Arduino示例教程模塊版——開啟您的Aduino之旅
    如何用arduino連接到電腦下載您的第一個例程。的COM口了如果你無法正常安裝驅動,可以參考:windows8下驅動安裝方法:http://www.arduino.cn/thread-2235-1-1.html arduino驅動安裝失敗的解決方法http://www.arduino.cn/thread-2485-1-1.html   5、啟動arduino應用程式
  • 腦電波頭環與Arduino如何造物?(內附詳細步驟)
    那麼,腦電波如何控制RGB燈呢?第一步,需要在arduino上連接一個4.0以上的藍牙模塊,用於接收腦波信號           第二步,連接RGB燈模塊          第四步,手機APP連接arduino的藍牙模塊
  • Arduino串口通信簡介
    ;  delay(100);}上面代碼將從arduino通過串口傳送字符串到電腦端的串口監視器上。發送數據串口監視器上方有一個輸入框和一個send按鈕,在輸入框內輸入內容,點擊send按鈕就可以向arduino發送數據了。
  • 間諜遊戲:用Arduino製作硬體鍵盤記錄器
    0×01 永遠跟不上的大牛腳步我是總部的Q博士,因為劉尼瑪的木馬程序被全部查殺,所以他需要一個永遠不可能被殺毒軟體查到的木馬,這個任務自然又落到了我的身上。當還是小菜的我好不容易用vb寫出第一個木馬,加載到註冊表開機啟動時,大牛嗤之以鼻,他說現在我們都玩進程注入了,這個早過時了;當我好不容易鼓搗成功dll木馬,準備慶賀的時候,大牛又給我潑了一盆冷水,他說現在是bootkit和rootkit的時代,傳統的木馬已經進歷史的垃圾堆了;當我狂啃下了一堆內核書籍,終於知道rootkit是怎麼回事的時候,大牛用嘲笑加可憐的眼神看著我說
  • 好玩的Python——Python玩轉Arduino
    前言Python玩轉arduino的方式跟mblock的在線編程模式差不多的,都是先給arduino寫入一個固件,然後操作,不同的是mblock是通過積木來向arduino下指令,這裡我們用Python.
  • Arduino為什麼這麼火
    正文:這一兩年間,arduino作為一個能夠快速表現設計想法的工具,日漸火了起來,愛好者們自發性、非官方組織起來的論壇社區日漸豐富和活躍,近一兩年小夥伴們的作品集中也越來越多地涉及arduino相關的項目案例,那麼我們就簡單地來看一下
  • 【Arduino教程】第一講:Arduino是什麼?
    如何學習Arduino?Arduino近幾年在國際發展火熱,教程也是五花八門。如果您英語頂呱呱,那推薦到arduino官方網站學習www.arduino.cc.英語不好,或者喜歡看中文教程的,就可以在論壇閱讀中文教程(傳送門:http://www.arduino.cn/thread-1066-1-1.html)認識Arduino UNOArduino UNO是Arduino入門的最佳選擇,在編著本書時,其最新的版本為UNO R3,本書大部分內容都是基於
  • 【arduino】流水燈
    最近球球小朋友對arduino產生了濃厚的興趣,沒事就找書或者找視頻進行學習。
  • 使用樹莓派做ROS開發_(4)搭建Arduino開發環境
    本次教程演示如何在樹莓派系統中搭建arduino的開發環境,完成本次教程需要準備arduino開發版一個,任何型號都行,我這裡使用的是UNO,下面是具體的搭建開發環境的步驟