Làm thế nào để ngắt hoạt động trên Arduino Uno và các bảng tương tự?


11

Vui lòng giải thích cách ngắt hoạt động trên Arduino Uno và các bo mạch liên quan bằng bộ xử lý ATmega328P. Các bảng như:

  • Đoàn kết
  • Nhỏ
  • Nano
  • Chuyên nghiệp nhỏ
  • Lilypad

Đặc biệt vui lòng thảo luận:

  • Sử dụng ngắt để làm gì
  • Cách viết một thói quen dịch vụ ngắt (ISR)
  • Vấn đề thời gian
  • Phần quan trọng
  • Nguyên tử truy cập dữ liệu

Lưu ý: đây là một câu hỏi tham khảo .

Câu trả lời:


25

TL; DR:

Khi viết một thói quen dịch vụ ngắt (ISR):

  • Giữ nó ngắn
  • Đừng dùng delay ()
  • Đừng in nối tiếp
  • Làm cho các biến được chia sẻ với mã chính biến động
  • Các biến được chia sẻ với mã chính có thể cần được bảo vệ bởi "các phần quan trọng" (xem bên dưới)
  • Đừng cố gắng tắt hoặc tắt

Ngắt là gì?

Hầu hết các bộ xử lý có ngắt. Ngắt cho phép bạn phản ứng với các sự kiện "bên ngoài" trong khi làm việc khác. Ví dụ, nếu bạn đang nấu bữa tối, bạn có thể cho khoai tây vào nấu trong 20 phút. Thay vì nhìn chằm chằm vào đồng hồ trong 20 phút, bạn có thể đặt hẹn giờ và sau đó đi xem TV. Khi đồng hồ bấm giờ, bạn "ngắt" việc xem TV để làm gì đó với khoai tây.


Ví dụ về ngắt

const byte LED = 13;
const byte SWITCH = 2;

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  if (digitalRead (SWITCH) == HIGH)
    digitalWrite (LED, HIGH);
  else
    digitalWrite (LED, LOW);
}  // end of switchPressed

void setup ()
{
  pinMode (LED, OUTPUT);  // so we can update the LED
  pinMode (SWITCH, INPUT_PULLUP);
  attachInterrupt (digitalPinToInterrupt (SWITCH), switchPressed, CHANGE);  // attach interrupt handler
}  // end of setup

void loop ()
{
  // loop doing nothing
}

Ví dụ này cho thấy làm thế nào, mặc dù vòng lặp chính không làm gì, bạn có thể bật hoặc tắt đèn LED ở chân 13, nếu nhấn công tắc trên chân D2.

Để kiểm tra điều này, chỉ cần kết nối một dây (hoặc công tắc) giữa D2 và Ground. Pullup bên trong (được bật trong thiết lập) buộc pin CAO bình thường. Khi tiếp đất, nó trở nên THẤP. Sự thay đổi trong pin được phát hiện bởi một ngắt CHANGE, điều này khiến cho Routine Service Routine (ISR) được gọi.

Trong một ví dụ phức tạp hơn, vòng lặp chính có thể đang làm một việc gì đó hữu ích, như lấy số đọc nhiệt độ và cho phép trình xử lý ngắt phát hiện một nút được ấn.


Chuyển đổi số pin thành số ngắt

Để đơn giản hóa việc chuyển đổi số vectơ ngắt thành số pin, bạn có thể gọi hàm digitalPinToInterrupt(), truyền số pin. Nó trả về số ngắt thích hợp, hoặc NOT_AN_INTERRUPT(-1).

Ví dụ: trên Uno, chân D2 trên bảng bị ngắt 0 (INT0_vect từ bảng bên dưới).

Do đó hai dòng này có cùng tác dụng:

  attachInterrupt (0, switchPressed, CHANGE);    // that is, for pin D2
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);

Tuy nhiên, cái thứ hai dễ đọc hơn và dễ di chuyển hơn với các loại Arduino khác nhau.


Ngắt có sẵn

Dưới đây là danh sách các ngắt, theo thứ tự ưu tiên, cho Atmega328:

 1  Reset
 2  External Interrupt Request 0  (pin D2)          (INT0_vect)
 3  External Interrupt Request 1  (pin D3)          (INT1_vect)
 4  Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
 5  Pin Change Interrupt Request 1 (pins A0 to A5)  (PCINT1_vect)
 6  Pin Change Interrupt Request 2 (pins D0 to D7)  (PCINT2_vect)
 7  Watchdog Time-out Interrupt                     (WDT_vect)
 8  Timer/Counter2 Compare Match A                  (TIMER2_COMPA_vect)
 9  Timer/Counter2 Compare Match B                  (TIMER2_COMPB_vect)
10  Timer/Counter2 Overflow                         (TIMER2_OVF_vect)
11  Timer/Counter1 Capture Event                    (TIMER1_CAPT_vect)
12  Timer/Counter1 Compare Match A                  (TIMER1_COMPA_vect)
13  Timer/Counter1 Compare Match B                  (TIMER1_COMPB_vect)
14  Timer/Counter1 Overflow                         (TIMER1_OVF_vect)
15  Timer/Counter0 Compare Match A                  (TIMER0_COMPA_vect)
16  Timer/Counter0 Compare Match B                  (TIMER0_COMPB_vect)
17  Timer/Counter0 Overflow                         (TIMER0_OVF_vect)
18  SPI Serial Transfer Complete                    (SPI_STC_vect)
19  USART Rx Complete                               (USART_RX_vect)
20  USART, Data Register Empty                      (USART_UDRE_vect)
21  USART, Tx Complete                              (USART_TX_vect)
22  ADC Conversion Complete                         (ADC_vect)
23  EEPROM Ready                                    (EE_READY_vect)
24  Analog Comparator                               (ANALOG_COMP_vect)
25  2-wire Serial Interface  (I2C)                  (TWI_vect)
26  Store Program Memory Ready                      (SPM_READY_vect)

Tên nội bộ (mà bạn có thể sử dụng để thiết lập các cuộc gọi lại ISR) nằm trong ngoặc.

Cảnh báo: Nếu bạn viết sai tên vectơ ngắt, thậm chí chỉ cần viết sai (một điều dễ làm), thói quen ngắt sẽ không được gọi và bạn sẽ không gặp lỗi trình biên dịch.


Lý do sử dụng ngắt

Những lý do chính bạn có thể sử dụng ngắt là:

  • Để phát hiện thay đổi pin (ví dụ: bộ mã hóa quay, nhấn nút)
  • Đồng hồ bấm giờ (ví dụ: nếu không có gì xảy ra sau 8 giây, làm gián đoạn tôi)
  • Ngắt hẹn giờ - được sử dụng để so sánh / tràn bộ định thời
  • Truyền dữ liệu SPI
  • Truyền dữ liệu I2C
  • Chuyển dữ liệu USART
  • Chuyển đổi ADC (tương tự sang kỹ thuật số)
  • EEPROM đã sẵn sàng để sử dụng
  • Bộ nhớ flash đã sẵn sàng

"Truyền dữ liệu" có thể được sử dụng để cho phép một chương trình làm việc khác trong khi dữ liệu được gửi hoặc nhận trên cổng nối tiếp, cổng SPI hoặc cổng I2C.

Đánh thức bộ xử lý

Ngắt bên ngoài, ngắt thay đổi pin và ngắt hẹn giờ theo dõi, cũng có thể được sử dụng để đánh thức bộ xử lý. Điều này có thể rất tiện dụng, vì ở chế độ ngủ, bộ xử lý có thể được cấu hình để sử dụng ít năng lượng hơn (ví dụ: khoảng 10 microamp). Có thể sử dụng ngắt tăng, giảm hoặc mức thấp để đánh thức một tiện ích (ví dụ: nếu bạn nhấn nút trên nó) hoặc ngắt "hẹn giờ theo dõi" có thể đánh thức nó theo định kỳ (ví dụ: để kiểm tra thời gian hoặc nhiệt độ).

Các ngắt thay đổi pin có thể được sử dụng để đánh thức bộ xử lý nếu một phím được nhấn trên bàn phím hoặc tương tự.

Bộ xử lý cũng có thể được đánh thức bằng một ngắt hẹn giờ (ví dụ: bộ định thời đạt đến một giá trị nhất định hoặc tràn) và một số sự kiện khác, chẳng hạn như tin nhắn I2C đến.


Kích hoạt / vô hiệu hóa các ngắt

Ngắt "thiết lập lại" không thể bị vô hiệu hóa. Tuy nhiên, các ngắt khác có thể tạm thời bị vô hiệu hóa bằng cách xóa cờ ngắt toàn cầu.

Kích hoạt ngắt

Bạn có thể bật ngắt với chức năng gọi "ngắt" hoặc "sei" như thế này:

interrupts ();  // or ...
sei ();         // set interrupts flag

Vô hiệu hóa ngắt

Nếu bạn cần tắt ngắt, bạn có thể "xóa" cờ ngắt toàn cục như thế này:

noInterrupts ();  // or ...
cli ();           // clear interrupts flag

Cả hai phương pháp đều có tác dụng tương tự, sử dụng interrupts/ noInterruptsdễ dàng hơn một chút để nhớ cách xung quanh chúng.

Mặc định trong Arduino là cho các ngắt được kích hoạt. Đừng vô hiệu hóa chúng trong thời gian dài hoặc những thứ như bộ hẹn giờ sẽ không hoạt động đúng.

Tại sao vô hiệu hóa ngắt?

Có thể có những đoạn mã quan trọng về thời gian mà bạn không muốn bị gián đoạn, ví dụ như bởi một ngắt thời gian.

Ngoài ra, nếu các trường nhiều byte đang được ISR cập nhật thì bạn có thể cần phải tắt các ngắt để bạn có được dữ liệu "nguyên tử". Mặt khác, một byte có thể được ISR cập nhật trong khi bạn đang đọc một byte khác.

Ví dụ:

noInterrupts ();
long myCounter = isrCounter;  // get value set by ISR
interrupts ();

Tắt tạm thời các ngắt đảm bảo rằng isrCorer (bộ đếm được đặt bên trong ISR) không thay đổi trong khi chúng ta đang nhận được giá trị của nó.

Cảnh báo: nếu bạn không chắc chắn các ngắt đã được bật hay chưa, thì bạn cần lưu trạng thái hiện tại và khôi phục nó sau đó. Ví dụ, mã từ hàm millis () thực hiện điều này:

unsigned long millis()
{
  unsigned long m;
  uint8_t oldSREG = SREG;    // <--------- save status register

  // disable interrupts while we read timer0_millis or we might get an
  // inconsistent value (e.g. in the middle of a write to timer0_millis)
  cli();
  m = timer0_millis;
  SREG = oldSREG;            // <---------- restore status register including interrupt flag

  return m;
}

Lưu ý các dòng được chỉ định lưu SREG hiện tại (thanh ghi trạng thái) bao gồm cờ ngắt. Sau khi chúng tôi đã nhận được giá trị bộ định thời gian (dài 4 byte), chúng tôi đặt lại thanh ghi trạng thái.


Lời khuyên

Tên hàm

Các chức năng cli/ seivà SREG đăng ký là dành riêng cho bộ xử lý AVR. Nếu bạn đang sử dụng các bộ xử lý khác như ARM, các chức năng có thể hơi khác một chút.

Vô hiệu hóa toàn cầu và vô hiệu hóa một ngắt

Nếu bạn sử dụng, cli()bạn vô hiệu hóa tất cả các ngắt (bao gồm cả ngắt hẹn giờ, ngắt nối tiếp, v.v.).

Tuy nhiên, nếu bạn chỉ muốn vô hiệu hóa một ngắt cụ thể thì bạn nên xóa cờ kích hoạt ngắt cho nguồn ngắt cụ thể đó. Ví dụ, đối với các ngắt ngoài, hãy gọi detachInterrupt().


Ưu tiên ngắt là gì?

Vì có 25 ngắt (trừ thiết lập lại), có thể có nhiều hơn một sự kiện ngắt có thể xảy ra cùng một lúc, hoặc ít nhất, xảy ra trước khi xử lý trước đó được xử lý. Ngoài ra một sự kiện ngắt có thể xảy ra trong khi ngắt bị vô hiệu hóa.

Thứ tự ưu tiên là chuỗi trong đó bộ xử lý kiểm tra các sự kiện ngắt. Danh sách càng cao, mức độ ưu tiên càng cao. Vì vậy, ví dụ, Yêu cầu ngắt ngoài 0 (chân D2) sẽ được phục vụ trước Yêu cầu ngắt ngoài 1 (chân D3).


Ngắt có thể xảy ra trong khi ngắt bị vô hiệu hóa?

Làm gián đoạn các sự kiện (nghĩa là thông báo sự kiện) có thể xảy ra bất cứ lúc nào và hầu hết được ghi nhớ bằng cách đặt cờ "sự kiện gián đoạn" bên trong bộ xử lý. Nếu các ngắt bị vô hiệu hóa, thì ngắt đó sẽ được xử lý khi chúng được bật lại, theo thứ tự ưu tiên.


Làm thế nào để bạn sử dụng ngắt?

  • Bạn viết một ISR (dịch vụ thường xuyên bị gián đoạn). Điều này được gọi khi sự gián đoạn xảy ra.
  • Bạn nói với bộ xử lý khi bạn muốn ngắt.

Viết ISR

Các tuyến dịch vụ ngắt là các hàm không có đối số. Một số thư viện Arduino được thiết kế để gọi các chức năng của riêng bạn, vì vậy bạn chỉ cần cung cấp một chức năng thông thường (như trong các ví dụ ở trên), ví dụ.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
 flag = true;
}  // end of switchPressed

Tuy nhiên, nếu thư viện chưa cung cấp "hook" cho ISR, bạn có thể tự tạo, như thế này:

volatile char buf [100];
volatile byte pos;

// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;  // grab byte from SPI Data Register

  // add to buffer if room
  if (pos < sizeof buf)
    {
    buf [pos++] = c;
    }  // end of room available
}  // end of interrupt routine SPI_STC_vect

Trong trường hợp này, bạn sử dụng macro "ISR" và cung cấp tên của vectơ ngắt có liên quan (từ bảng trước đó). Trong trường hợp này, ISR đang xử lý hoàn tất chuyển khoản SPI. (Lưu ý, một số mã cũ sử dụng TÍN HIỆU thay vì ISR, tuy nhiên TÍN HIỆU không được chấp nhận).

Kết nối ISR ​​với ngắt

Đối với các ngắt đã được xử lý bởi các thư viện, bạn chỉ cần sử dụng giao diện tài liệu. Ví dụ:

void receiveEvent (int howMany)
 {
  while (Wire.available () > 0)
    {
    char c = Wire.receive ();
    // do something with the incoming byte
    }
}  // end of receiveEvent

void setup ()
  {
  Wire.onReceive(receiveEvent);
  }

Trong trường hợp này, thư viện I2C được thiết kế để xử lý các byte I2C đến bên trong, sau đó gọi hàm được cung cấp của bạn ở cuối luồng dữ liệu đến. Trong trường hợp này receiveEvent không hoàn toàn là ISR (nó có đối số) nhưng nó được gọi bởi ISR ​​sẵn có.

Một ví dụ khác là ngắt "pin ngoài".

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
}  // end of switchPressed

void setup ()
{
  attachInterrupt (digitalPinToInterrupt (2), switchPressed, CHANGE);  // attach interrupt handler for D2
}  // end of setup

Trong trường hợp này, hàm Đính kèm sẽ thêm chức năng switchPress vào bảng bên trong và ngoài ra, cấu hình các cờ ngắt thích hợp trong bộ xử lý.

Cấu hình bộ xử lý để xử lý ngắt

Bước tiếp theo, một khi bạn có ISR, là nói với bộ xử lý rằng bạn muốn điều kiện đặc biệt này tăng lên một ngắt.

Ví dụ, đối với Ngắt bên ngoài 0 (ngắt D2), bạn có thể làm một cái gì đó như thế này:

EICRA &= ~3;  // clear existing flags
EICRA |= 2;   // set wanted flags (falling level interrupt)
EIMSK |= 1;   // enable it

Dễ đọc hơn sẽ là sử dụng các tên được xác định, như thế này:

EICRA &= ~(bit(ISC00) | bit (ISC01));  // clear existing flags
EICRA |= bit (ISC01);    // set wanted flags (falling level interrupt)
EIMSK |= bit (INT0);     // enable it

EICRA (Thanh ghi điều khiển ngắt ngoài A) sẽ được đặt theo bảng này từ biểu dữ liệu Atmega328. Điều đó xác định loại ngắt chính xác mà bạn muốn:

  • 0: Mức INT0 thấp tạo ra yêu cầu ngắt (ngắt THẤP).
  • 1: Bất kỳ thay đổi logic nào trên INT0 đều tạo ra yêu cầu ngắt (ngắt CHANGE).
  • 2: Cạnh rơi của INT0 tạo ra yêu cầu ngắt (ngắt FALLING).
  • 3: Cạnh tăng của INT0 tạo ra yêu cầu ngắt (ngắt RISING).

EIMSK (Thanh ghi ngắt mặt ngoài) thực sự cho phép ngắt.

May mắn thay, bạn không cần phải nhớ những con số đó bởi vì Đính kèm sẽ thực hiện điều đó cho bạn. Tuy nhiên, đó là những gì đang thực sự xảy ra và đối với các ngắt khác, bạn có thể phải "đặt thủ công" các cờ ngắt.


ISR cấp thấp so với ISR ​​thư viện

Để đơn giản hóa cuộc sống của bạn, một số trình xử lý ngắt phổ biến thực sự nằm trong mã thư viện (ví dụ INT0_vect và INT1_vect) và sau đó một giao diện thân thiện hơn được cung cấp (ví dụ: Đính kèm). Những gì Đính kèm thực sự làm là lưu địa chỉ của trình xử lý ngắt mong muốn của bạn vào một biến, và sau đó gọi nó từ INT0_vect / INT1_vect khi cần. Nó cũng đặt các cờ đăng ký thích hợp để gọi trình xử lý khi được yêu cầu.


ISR có thể bị gián đoạn?

Tóm lại, không, trừ khi bạn muốn họ trở thành.

Khi ISR ​​được nhập, ngắt bị vô hiệu hóa . Đương nhiên, chúng phải được kích hoạt ngay từ đầu, nếu không ISR sẽ không được nhập. Tuy nhiên, để tránh việc ISR bị gián đoạn, bộ xử lý sẽ ngắt.

Khi ISR ​​thoát, sau đó ngắt được bật lại . Trình biên dịch cũng tạo mã bên trong ISR để lưu các thanh ghi và cờ trạng thái, do đó, bất cứ điều gì bạn đang làm khi xảy ra gián đoạn sẽ không bị ảnh hưởng.

Tuy nhiên, bạn có thể bật ngắt trong ISR nếu bạn hoàn toàn phải, ví dụ.

// Interrupt Service Routine (ISR)
void switchPressed ()
{
  // handle pin change here
  interrupts ();  // allow more interrupts

}  // end of switchPressed

Thông thường bạn sẽ cần một lý do khá chính đáng để làm điều này, vì một ngắt khác bây giờ có thể dẫn đến một cuộc gọi đệ quy đến pinChange, với kết quả khá không mong muốn.


Mất bao lâu để thực hiện ISR?

Theo biểu dữ liệu, lượng thời gian tối thiểu để phục vụ ngắt là 4 chu kỳ xung nhịp (để đẩy bộ đếm chương trình hiện tại lên ngăn xếp) theo sau là mã hiện đang thực thi tại vị trí vectơ ngắt. Điều này thường chứa một bước nhảy đến nơi mà thói quen ngắt thực sự là 3 chu kỳ khác. Việc kiểm tra mã do trình biên dịch tạo ra cho thấy rằng một ISR được tạo bằng khai báo "ISR" có thể mất khoảng 2.625 để thực thi, cộng với bất cứ điều gì mà chính mã đó làm. Số tiền chính xác phụ thuộc vào số lượng đăng ký cần được lưu và khôi phục. Số tiền tối thiểu sẽ là 1.1875 lượt.

Các ngắt ngoài (nơi bạn sử dụng Đính kèm) làm thêm một chút và mất tổng cộng khoảng 5.125 (chạy với xung nhịp 16 MHz).


Mất bao lâu trước khi bộ xử lý bắt đầu nhập ISR?

Điều này thay đổi phần nào. Các số liệu được trích dẫn ở trên là những con số lý tưởng trong đó ngắt được xử lý ngay lập tức. Một vài yếu tố có thể trì hoãn rằng:

  • Nếu bộ xử lý đang ngủ, có những khoảng thời gian "thức dậy" được chỉ định, có thể là vài mili giây, trong khi đồng hồ được quay ngược trở lại tốc độ. Thời gian này sẽ phụ thuộc vào cài đặt cầu chì và giấc ngủ sâu đến mức nào.

  • Nếu một thói quen dịch vụ ngắt đã được thực thi, thì các ngắt tiếp theo không thể được nhập cho đến khi nó kết thúc hoặc cho phép tự ngắt. Đây là lý do tại sao bạn nên duy trì mỗi thói quen dịch vụ gián đoạn ngắn, vì mỗi micro giây bạn dành cho một, bạn có khả năng trì hoãn việc thực hiện một dịch vụ khác.

  • Một số mã tắt ngắt. Ví dụ, gọi millis () nhanh chóng tắt ngắt. Do đó, thời gian cho một ngắt được phục vụ sẽ được kéo dài bởi thời gian ngắt được tắt.

  • Ngắt chỉ có thể được phục vụ khi kết thúc một lệnh, vì vậy nếu một lệnh cụ thể mất ba chu kỳ đồng hồ và vừa bắt đầu, thì ngắt sẽ bị trì hoãn ít nhất một vài chu kỳ đồng hồ.

  • Một sự kiện bật ngắt trở lại (ví dụ: trở về từ thói quen dịch vụ ngắt) được đảm bảo thực hiện ít nhất một lệnh nữa. Vì vậy, ngay cả khi ISR ​​kết thúc và ngắt của bạn đang chờ xử lý, nó vẫn phải chờ thêm một hướng dẫn trước khi được phục vụ.

  • Vì các ngắt có mức độ ưu tiên, nên ngắt có mức ưu tiên cao hơn có thể được phục vụ trước khi ngắt mà bạn quan tâm.


Cân nhắc hiệu suất

Ngắt có thể tăng hiệu suất trong nhiều tình huống vì bạn có thể tiếp tục với "công việc chính" của chương trình mà không phải liên tục kiểm tra để xem các công tắc đã được nhấn chưa. Đã nói rằng, chi phí chung của việc phục vụ một ngắt, như đã thảo luận ở trên, thực sự sẽ nhiều hơn là thực hiện một "vòng lặp chặt chẽ" bỏ phiếu cho một cổng đầu vào. Bạn chỉ có thể trả lời một sự kiện trong vòng một phần triệu giây. Trong trường hợp đó, bạn có thể vô hiệu hóa các ngắt (ví dụ: bộ định thời) và chỉ cần vòng tìm kiếm pin để thay đổi.


Làm thế nào được ngắt xếp hàng?

Có hai loại ngắt:

  • Một số đặt cờ và chúng được xử lý theo thứ tự ưu tiên, ngay cả khi sự kiện gây ra chúng đã dừng lại. Ví dụ, ngắt cấp tăng, giảm hoặc thay đổi trên chân D2.

  • Những người khác chỉ được kiểm tra nếu họ đang xảy ra "ngay bây giờ". Ví dụ, ngắt cấp thấp trên chân D2.

Những cái đặt cờ có thể được coi là được xếp hàng, vì cờ ngắt vẫn được đặt cho đến khi thời gian ngắt được nhập, tại thời điểm bộ xử lý xóa cờ. Tất nhiên, vì chỉ có một cờ, nếu điều kiện ngắt tương tự xảy ra lần nữa trước khi cờ đầu tiên được xử lý, nó sẽ không được phục vụ hai lần.

Một điều cần lưu ý là những cờ này có thể được đặt trước khi bạn gắn trình xử lý ngắt. Ví dụ, có thể làm gián đoạn mức tăng hoặc giảm trên chân D2 bị "gắn cờ", và ngay sau khi bạn thực hiện đính kèm, ngắt ngay lập tức, ngay cả khi sự kiện xảy ra cách đây một giờ. Để tránh điều này, bạn có thể tự xóa cờ. Ví dụ:

EIFR = bit (INTF0);  // clear flag for interrupt 0
EIFR = bit (INTF1);  // clear flag for interrupt 1

Tuy nhiên, các ngắt "mức thấp" liên tục được kiểm tra, vì vậy nếu bạn không cẩn thận, chúng sẽ tiếp tục bắn, ngay cả sau khi ngắt được gọi. Đó là, ISR sẽ thoát, và sau đó ngắt sẽ ngay lập tức bắn lại. Để tránh điều này, bạn nên thực hiện tách rời ngay lập tức sau khi bạn biết rằng ngắt bị bắn.


Gợi ý viết ISR

Tóm lại, hãy giữ chúng ngắn gọn! Trong khi ISR ​​đang thực thi các ngắt khác không thể được xử lý. Vì vậy, bạn có thể dễ dàng bỏ lỡ các lần nhấn nút hoặc liên lạc nối tiếp đến, nếu bạn cố gắng làm quá nhiều. Cụ thể, bạn không nên thử gỡ lỗi "bản in" bên trong ISR. Thời gian để làm những việc đó có khả năng gây ra nhiều vấn đề hơn những gì họ giải quyết.

Một điều hợp lý để làm là đặt cờ đơn byte, sau đó kiểm tra cờ đó trong hàm vòng lặp chính. Hoặc, lưu trữ một byte đến từ một cổng nối tiếp vào bộ đệm. Bộ đếm thời gian sẵn có theo dõi thời gian đã trôi qua bằng cách bắn mỗi khi bộ định thời bên trong tràn ra, và do đó bạn có thể xử lý thời gian đã trôi qua bằng cách biết số lần bộ hẹn giờ bị tràn.

Hãy nhớ rằng, bên trong một ngắt ISR bị vô hiệu hóa. Do đó, hy vọng rằng thời gian được trả về bởi các lệnh gọi hàm millis () sẽ thay đổi, sẽ dẫn đến sự thất vọng. Nó là hợp lệ để có được thời gian theo cách đó, chỉ cần lưu ý rằng bộ đếm thời gian không tăng. Và nếu bạn sử dụng ISR quá lâu thì bộ hẹn giờ có thể bỏ lỡ một sự kiện tràn, dẫn đến thời gian được trả về bởi millis () trở nên không chính xác.

Một thử nghiệm cho thấy, trên bộ xử lý Atmega328 16 MHz, một cuộc gọi đến micros () mất 3,5625. Một cuộc gọi đến millis () mất 1.9375. Ghi (lưu) giá trị bộ định thời hiện tại là điều hợp lý để thực hiện trong ISR. Tìm kiếm mili giây trôi qua nhanh hơn so với micro giây trôi qua (số mili giây chỉ được lấy từ một biến). Tuy nhiên, số lượng micrô giây thu được bằng cách thêm giá trị hiện tại của bộ định thời Timer 0 (sẽ tiếp tục tăng) vào "số đếm tràn 0 Timer" đã lưu.

Cảnh báo: Do các ngắt bị vô hiệu hóa bên trong ISR và vì phiên bản mới nhất của Arduino IDE sử dụng các ngắt để đọc và ghi nối tiếp và cũng để tăng bộ đếm được sử dụng bởi "millis" và "delay", bạn không nên cố gắng sử dụng các chức năng đó bên trong một ISR. Theo một cách khác:

  • Đừng cố trì hoãn, ví dụ: delay (100);
  • Bạn có thể nhận được thời gian từ một cuộc gọi đến millis, tuy nhiên nó sẽ không tăng, vì vậy đừng cố trì hoãn bằng cách chờ đợi nó tăng lên.
  • Đừng in nối tiếp (ví dụ. Serial.println ("ISR entered");)
  • Đừng cố đọc nối tiếp.

Ngắt thay đổi pin

Có hai cách bạn có thể phát hiện các sự kiện bên ngoài trên các chân. Đầu tiên là các chân "ngắt ngoài" đặc biệt, D2 và D3. Những sự kiện ngắt rời rạc chung này, mỗi sự kiện một pin. Bạn có thể nhận được những thứ đó bằng cách sử dụng Đính kèm cho mỗi pin. Bạn có thể chỉ định một điều kiện tăng, giảm, thay đổi hoặc mức độ thấp cho ngắt.

Tuy nhiên, cũng có các ngắt "thay đổi pin" cho tất cả các chân (trên Atmega328, không nhất thiết là tất cả các chân trên các bộ xử lý khác). Những hành động này trên các nhóm chân (D0 đến D7, D8 đến D13 và A0 đến A5). Họ cũng có mức độ ưu tiên thấp hơn các ngắt sự kiện bên ngoài. Tuy nhiên, chúng hơi khó sử dụng hơn các ngắt bên ngoài vì chúng được nhóm thành các đợt. Vì vậy, nếu ngắt xảy ra, bạn phải tìm ra mã của chính xác chính xác pin nào gây ra ngắt.

Mã ví dụ:

ISR (PCINT0_vect)
 {
 // handle pin change interrupt for D8 to D13 here
 }  // end of PCINT0_vect

ISR (PCINT1_vect)
 {
 // handle pin change interrupt for A0 to A5 here
 }  // end of PCINT1_vect

ISR (PCINT2_vect)
 {
 // handle pin change interrupt for D0 to D7 here
 }  // end of PCINT2_vect


void setup ()
  {
  // pin change interrupt (example for D9)
  PCMSK0 |= bit (PCINT1);  // want pin 9
  PCIFR  |= bit (PCIF0);   // clear any outstanding interrupts
  PCICR  |= bit (PCIE0);   // enable pin change interrupts for D8 to D13
  }

Để xử lý ngắt thay đổi pin, bạn cần:

  • Chỉ định pin nào trong nhóm. Đây là biến PCMSKn (trong đó n là 0, 1 hoặc 2 từ bảng bên dưới). Bạn có thể có các ngắt trên nhiều hơn một pin.
  • Cho phép nhóm ngắt thích hợp (0, 1 hoặc 2)
  • Cung cấp một trình xử lý ngắt như hình trên

Bảng chân -> tên thay đổi pin / mặt nạ

D0    PCINT16 (PCMSK2 / PCIF2 / PCIE2)
D1    PCINT17 (PCMSK2 / PCIF2 / PCIE2)
D2    PCINT18 (PCMSK2 / PCIF2 / PCIE2)
D3    PCINT19 (PCMSK2 / PCIF2 / PCIE2)
D4    PCINT20 (PCMSK2 / PCIF2 / PCIE2)
D5    PCINT21 (PCMSK2 / PCIF2 / PCIE2)
D6    PCINT22 (PCMSK2 / PCIF2 / PCIE2)
D7    PCINT23 (PCMSK2 / PCIF2 / PCIE2)
D8    PCINT0  (PCMSK0 / PCIF0 / PCIE0)
D9    PCINT1  (PCMSK0 / PCIF0 / PCIE0)
D10   PCINT2  (PCMSK0 / PCIF0 / PCIE0)
D11   PCINT3  (PCMSK0 / PCIF0 / PCIE0)
D12   PCINT4  (PCMSK0 / PCIF0 / PCIE0)
D13   PCINT5  (PCMSK0 / PCIF0 / PCIE0)
A0    PCINT8  (PCMSK1 / PCIF1 / PCIE1)
A1    PCINT9  (PCMSK1 / PCIF1 / PCIE1)
A2    PCINT10 (PCMSK1 / PCIF1 / PCIE1)
A3    PCINT11 (PCMSK1 / PCIF1 / PCIE1)
A4    PCINT12 (PCMSK1 / PCIF1 / PCIE1)
A5    PCINT13 (PCMSK1 / PCIF1 / PCIE1)

Xử lý ngắt

Trình xử lý ngắt sẽ cần tìm ra chân nào gây ra ngắt nếu mặt nạ chỉ định nhiều hơn một (ví dụ: nếu bạn muốn ngắt trên D8 / D9 / D10). Để làm điều này, bạn sẽ cần lưu trữ trạng thái trước đó của mã pin đó và xử lý (bằng cách thực hiện DigitalRead hoặc tương tự) nếu mã pin cụ thể này đã thay đổi.


Có lẽ bạn đang sử dụng ngắt ...

Một môi trường Arduino "bình thường" đã sử dụng các ngắt, ngay cả khi bạn không cố gắng cá nhân. Các lệnh gọi hàm millis () và micros () sử dụng tính năng "tràn bộ đếm thời gian". Một trong các bộ định thời bên trong (bộ định thời 0) được thiết lập để ngắt khoảng 1000 lần một giây và tăng bộ đếm bên trong có hiệu quả trở thành bộ đếm millis (). Có nhiều hơn một chút so với điều đó, vì điều chỉnh được thực hiện cho tốc độ đồng hồ chính xác.

Ngoài ra, thư viện nối tiếp phần cứng sử dụng các ngắt để xử lý dữ liệu nối tiếp đến và đi. Điều này rất hữu ích vì chương trình của bạn có thể thực hiện những việc khác trong khi các ngắt đang kích hoạt và lấp đầy bộ đệm bên trong. Sau đó, khi bạn kiểm tra Serial.av Available (), bạn có thể tìm hiểu xem, nếu có bất cứ thứ gì, đã được đặt trong bộ đệm đó.


Thực hiện hướng dẫn tiếp theo sau khi kích hoạt ngắt

Sau một chút thảo luận và nghiên cứu trên diễn đàn Arduino, chúng tôi đã làm rõ chính xác những gì xảy ra sau khi bạn kích hoạt ngắt. Có ba cách chính tôi có thể nghĩ là bạn có thể kích hoạt các ngắt mà trước đây không được kích hoạt:

  sei ();  // set interrupt enable flag
  SREG |= 0x80;  // set the high-order bit in the status register
  reti  ;   // assembler instruction "return from interrupt"

Trong mọi trường hợp, bộ xử lý đảm bảo rằng lệnh tiếp theo sau khi ngắt được bật (nếu chúng bị tắt trước đó) sẽ luôn được thực thi, ngay cả khi sự kiện ngắt đang chờ xử lý. .

Điều này cho phép bạn viết mã như thế này:

sei ();
sleep_cpu ();

Nếu không đảm bảo, sự gián đoạn này có thể xảy ra trước khi bộ xử lý ngủ, và sau đó nó có thể không bao giờ bị đánh thức.


Ngắt trống

Nếu bạn chỉ muốn ngắt để đánh thức bộ xử lý, nhưng không làm gì cụ thể, bạn có thể sử dụng định nghĩa EMPTY_INTERRUPT, ví dụ.

EMPTY_INTERRUPT (PCINT1_vect);

Điều này chỉ đơn giản là tạo ra một lệnh "reti" (trở lại từ ngắt). Vì nó không cố lưu hoặc khôi phục các thanh ghi, đây sẽ là cách nhanh nhất để làm gián đoạn đánh thức nó.


Phần quan trọng (truy cập biến nguyên tử)

Có một số vấn đề tế nhị liên quan đến các biến được chia sẻ giữa các thói quen dịch vụ ngắt (ISR) và mã chính (nghĩa là mã không nằm trong ISR).

Vì ISR có thể kích hoạt bất cứ lúc nào khi ngắt được kích hoạt, bạn cần thận trọng khi truy cập các biến được chia sẻ như vậy, vì chúng có thể được cập nhật tại thời điểm bạn truy cập chúng.

Đầu tiên ... khi nào bạn sử dụng biến "dễ bay hơi"?

Một biến chỉ nên được đánh dấu dễ bay hơi nếu nó được sử dụng cả bên trong ISR và bên ngoài một biến.

  • Các biến chỉ được sử dụng bên ngoài ISR ​​sẽ không biến động.
  • Các biến chỉ được sử dụng bên trong ISR sẽ không biến động.
  • Các biến được sử dụng cả bên trong và bên ngoài ISR sẽ không ổn định.

ví dụ.

volatile int counter;

Việc đánh dấu một biến là dễ bay hơi sẽ cho trình biên dịch không "lưu" nội dung biến vào một thanh ghi bộ xử lý, nhưng luôn đọc nó từ bộ nhớ, khi cần. Điều này có thể làm chậm quá trình xử lý, đó là lý do tại sao bạn không làm cho mọi biến số biến động, khi không cần thiết.

Tắt ngắt trong khi truy cập một biến dễ bay hơi

Ví dụ, để so sánh countvới một số số, tắt ngắt trong khi so sánh trong trường hợp một byte countđã được ISR cập nhật và không phải là byte khác.

volatile unsigned int count;

ISR (TIMER1_OVF_vect)
  {
  count++;
  } // end of TIMER1_OVF_vect

void setup ()
  {
  pinMode (13, OUTPUT);
  }  // end of setup

void loop ()
  {
  noInterrupts ();    // <------ critical section
  if (count > 20)
     digitalWrite (13, HIGH);
  interrupts ();      // <------ end critical section
  } // end of loop

Đọc bảng dữ liệu!

Thông tin thêm về các ngắt, bộ hẹn giờ, vv có thể được lấy từ bảng dữ liệu cho bộ xử lý.

http://www.atmel.com/images/Atmel-8271-8-bit-AVR-Microcontler-ATmega48A-48PA-88A-88PA-168A-168PA-328-328P_datasheet_Complete.pdf


Ví dụ khác

Cân nhắc không gian (giới hạn kích thước bài đăng) ngăn chặn danh sách mã ví dụ của tôi. Để biết thêm ví dụ mã xem trang của tôi về các ngắt .


Một tài liệu tham khảo rất hữu ích - đó là một câu trả lời nhanh chóng ấn tượng.
Đạt Han Bag

Đó là một câu hỏi tham khảo. Tôi đã chuẩn bị câu trả lời và nó sẽ còn nhanh hơn nữa nếu câu trả lời không quá dài, vì vậy tôi phải cắt tỉa lại. Xem trang web được liên kết để biết thêm chi tiết.
Nick Gammon

Về "chế độ ngủ" có hiệu quả để làm cho Arduino ngủ, giả sử, 500ms?
Đạt Hà

@Nick Gammon Tôi đoán bật hoặc tắt nguồn (có tự động hóa hay không) cho CPU có thể được định nghĩa là một ngắt độc đáo - nếu bạn muốn làm điều đó. "Tôi đã chuẩn bị câu trả lời" - bạn vừa lấy ra tất cả ma thuật của khoảnh khắc mà tôi nghĩ nó có.
Đạt Han Bag

1
Tôi sợ điều đó không đúng. Tôi có một ví dụ sử dụng ngắt thay đổi pin để đánh thức từ chế độ tắt nguồn. Ngoài ra, như tôi đã đề cập trên trang của mình về các ngắt Atmel đã xác nhận rằng bất kỳ ngắt ngoài nào cũng sẽ đánh thức bộ xử lý (tức là tăng / giảm / thay đổi thấp).
Nick Gammon
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.