Sự gián đoạn Arduino (thay đổi pin)


8

Tôi sử dụng hàm ngắt để điền vào một mảng với các giá trị nhận được từ digitalRead().

 void setup() {
      Serial.begin(115200);
       attachInterrupt(0, test_func, CHANGE);
    }

    void test_func(){
      if(digitalRead(pin)==HIGH){
          test_array[x]=1;  
        } else if(digitalRead(pin)==LOW){
          test_array[x]=0;  
        }
         x=x+1;
    }

Vấn đề là khi tôi in test_arraycó các giá trị như: 111hoặc 000.

Theo tôi hiểu, nếu tôi sử dụng CHANGEtùy chọn trong attachInterrupt()hàm, thì chuỗi dữ liệu sẽ luôn luôn 0101010101không bị lặp lại.

Dữ liệu thay đổi khá nhanh vì nó đến từ một mô-đun radio.


1
Ngắt không gỡ nút. Bạn đang sử dụng gỡ lỗi phần cứng?
Ignacio Vazquez-Abrams

Vui lòng gửi mã hoàn chỉnh, bao gồm pin, xtest_arrayđịnh nghĩa, và cả loop()phương pháp; nó sẽ cho phép chúng tôi xem liệu đây có thể là một vấn đề tương tranh khi truy cập các biến được sửa đổi bởi test_func.
jfpoilpret

2
Bạn không nên DigitalRead () hai lần trong ISR: suy nghĩ về những gì sẽ xảy ra nếu bạn nhận được THẤP ở cuộc gọi đầu tiên và CAO ở lần thứ hai. Thay vào đó, if (digitalRead(pin) == HIGH) ... else ...;hoặc, tốt hơn nữa, ISR một dòng này : test_array[x++] = digitalRead(pin);.
Edgar Bonet

@EdgarBonet đẹp! +1 cho nhận xét đó. Tôi hy vọng bạn không phiền tôi đã thêm một cái gì đó vào câu trả lời của tôi để bao gồm những gì bạn đã đề cập ở đây. Ngoài ra nếu bạn quyết định đưa ra câu trả lời của riêng mình bao gồm chi tiết này thì tôi sẽ xóa phần bổ sung của tôi và bỏ phiếu cho bạn để bạn nhận được đại diện cho nó.
Clayton Mills

@Clayton Mills: Tôi đang chuẩn bị một câu trả lời (quá dài và hơi tiếp tuyến), nhưng bạn có thể giữ bản chỉnh sửa của mình, nó hoàn toàn ổn với tôi.
Edgar Bonet

Câu trả lời:


20

Như một lời mở đầu cho câu trả lời quá dài này ...

Câu hỏi này khiến tôi say mê sâu sắc với vấn đề độ trễ gián đoạn, đến mức mất ngủ trong việc đếm chu kỳ thay vì cừu. Tôi viết thư trả lời này nhiều hơn để chia sẻ những phát hiện của tôi hơn là chỉ trả lời câu hỏi: hầu hết các tài liệu này thực sự có thể không ở mức phù hợp cho một câu trả lời thích hợp. Tôi hy vọng nó sẽ hữu ích, tuy nhiên, đối với những độc giả đến đây để tìm kiếm giải pháp cho các vấn đề về độ trễ. Một vài phần đầu tiên dự kiến ​​sẽ hữu ích cho nhiều đối tượng, bao gồm cả poster gốc. Sau đó, nó có lông trên đường đi.

Clayton Mills đã giải thích trong câu trả lời của mình rằng có một số độ trễ trong việc đáp ứng các ngắt. Ở đây tôi sẽ tập trung vào việc định lượng độ trễ ( rất lớn khi sử dụng các thư viện Arduino) và phương tiện để giảm thiểu nó. Hầu hết những gì sau đây là dành riêng cho phần cứng của Arduino Uno và các bo mạch tương tự.

Giảm thiểu độ trễ ngắt trên Arduino

(hoặc làm thế nào để có được từ 99 xuống đến 5 chu kỳ)

Tôi sẽ sử dụng câu hỏi ban đầu làm ví dụ hoạt động và trình bày lại vấn đề về độ trễ ngắt. Chúng tôi có một số sự kiện bên ngoài kích hoạt ngắt (ở đây: INT0 khi thay pin). Chúng ta cần thực hiện một số hành động khi ngắt được kích hoạt (ở đây: đọc một đầu vào kỹ thuật số). Vấn đề là: có một số độ trễ giữa ngắt được kích hoạt và chúng ta thực hiện hành động thích hợp. Chúng tôi gọi sự chậm trễ này là " độ trễ ngắt ". Một độ trễ dài là bất lợi trong nhiều tình huống. Trong ví dụ cụ thể này, tín hiệu đầu vào có thể thay đổi trong thời gian trễ, trong trường hợp đó chúng ta nhận được lỗi đọc. Không có gì chúng ta có thể làm để tránh sự chậm trễ: đó là bản chất của cách làm gián đoạn công việc. Tuy nhiên, chúng ta có thể cố gắng làm cho nó càng ngắn càng tốt, điều này hy vọng sẽ giảm thiểu hậu quả xấu.

Điều rõ ràng đầu tiên chúng ta có thể làm là thực hiện hành động quan trọng về thời gian, bên trong bộ xử lý ngắt, càng sớm càng tốt. Điều này có nghĩa là gọi digitalRead()một lần (và chỉ một lần) vào lúc bắt đầu xử lý. Đây là phiên bản zeroth của chương trình mà chúng tôi sẽ xây dựng:

#define INT_NUMBER 0
#define PIN_NUMBER 2    // interrupt 0 is on pin 2
#define MAX_COUNT  200

volatile uint8_t count_edges;  // count of signal edges
volatile uint8_t count_high;   // count of high levels

/* Interrupt handler. */
void read_pin()
{
    int pin_state = digitalRead(PIN_NUMBER);  // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (pin_state == HIGH) count_high++;
}

void setup()
{
    Serial.begin(9600);
    attachInterrupt(INT_NUMBER, read_pin, CHANGE);
}

void loop()
{
    /* Wait for the interrupt handler to count MAX_COUNT edges. */
    while (count_edges < MAX_COUNT) { /* wait */ }

    /* Report result. */
    Serial.print("Counted ");
    Serial.print(count_high);
    Serial.print(" HIGH levels for ");
    Serial.print(count_edges);
    Serial.println(" edges");

    /* Count again. */
    count_high = 0;
    count_edges = 0;  // do this last to avoid race condition
}

Tôi đã thử nghiệm chương trình này và các phiên bản tiếp theo bằng cách gửi cho nó các chuỗi xung có độ rộng khác nhau. Có đủ khoảng cách giữa các xung để đảm bảo không bỏ sót cạnh nào: ngay cả khi nhận được cạnh rơi trước khi ngắt trước đó, yêu cầu ngắt thứ hai sẽ được giữ và cuối cùng được phục vụ. Nếu một xung ngắn hơn độ trễ ngắt, chương trình sẽ đọc 0 trên cả hai cạnh. Số lượng mức CAO được báo cáo sau đó là tỷ lệ phần trăm của các xung đọc chính xác.

Điều gì xảy ra khi ngắt được kích hoạt?

Trước khi cố gắng cải thiện mã ở trên, chúng tôi sẽ xem xét các sự kiện diễn ra ngay sau khi ngắt được kích hoạt. Phần cứng của câu chuyện được kể bởi tài liệu Atmel. Phần mềm, bằng cách phân tách nhị phân.

Hầu hết thời gian, ngắt đến được phục vụ ngay lập tức. Tuy nhiên, có thể xảy ra rằng MCU (có nghĩa là "vi điều khiển") đang ở giữa một số nhiệm vụ quan trọng về thời gian, trong đó dịch vụ ngắt bị vô hiệu hóa. Đây thường là trường hợp khi nó đã phục vụ một ngắt khác. Khi điều này xảy ra, yêu cầu ngắt đến được giữ lại và chỉ được phục vụ khi phần quan trọng về thời gian đó được thực hiện. Tình huống này khó có thể tránh hoàn toàn, bởi vì có khá nhiều phần quan trọng trong thư viện lõi Arduino (mà tôi sẽ gọi là " libcore"trong phần sau). May mắn thay, các phần này ngắn và chỉ chạy thường xuyên. Vì vậy, hầu hết thời gian, yêu cầu ngắt của chúng tôi sẽ được phục vụ ngay lập tức. Trong phần sau, tôi sẽ cho rằng chúng tôi không quan tâm đến số ít đó trường hợp khi đây không phải là trường hợp.

Sau đó, yêu cầu của chúng tôi được phục vụ ngay lập tức. Điều này vẫn liên quan đến rất nhiều thứ có thể mất khá nhiều thời gian. Đầu tiên, có một chuỗi khó khăn. MCU sẽ hoàn thành việc thực hiện hướng dẫn hiện tại. May mắn thay, hầu hết các hướng dẫn là chu kỳ đơn, nhưng một số có thể mất tới bốn chu kỳ. Sau đó, MCU xóa một cờ bên trong không cho phép phục vụ thêm các ngắt. Điều này nhằm ngăn chặn các ngắt lồng nhau. Sau đó, PC được lưu vào ngăn xếp. Ngăn xếp là một vùng RAM dành riêng cho loại lưu trữ tạm thời này. PC (có nghĩa là " Bộ đếm chương trình") là một thanh ghi nội bộ giữ địa chỉ của lệnh tiếp theo mà MCU sắp thực hiện. Đây là điều cho phép MCU biết phải làm gì tiếp theo và lưu nó là điều cần thiết bởi vì nó sẽ phải được khôi phục để làm chính chương trình để tiếp tục từ nơi bị gián đoạn. PC sau đó được tải với một địa chỉ được xác định cụ thể theo yêu cầu nhận được, và đây là phần cuối của chuỗi cứng, phần còn lại được điều khiển bằng phần mềm.

MCU hiện thực hiện hướng dẫn từ địa chỉ cứng này. Hướng dẫn này được gọi là " vectơ ngắt " và nói chung là một lệnh "nhảy" sẽ đưa chúng ta đến một thói quen đặc biệt gọi là ISR ("Định tuyến dịch vụ ngắt "). Trong trường hợp này, ISR được gọi là "__vector_1", còn gọi là "INT0_vect", đây là một cách gọi sai vì đây là ISR, không phải là vectơ. ISR đặc biệt này đến từ libcore. Giống như bất kỳ ISR nào, nó bắt đầu bằng một phần mở đầu lưu một loạt các thanh ghi CPU bên trong trên ngăn xếp. Điều này sẽ cho phép nó sử dụng các thanh ghi đó và khi hoàn thành, khôi phục chúng về các giá trị trước đó để không làm phiền chương trình chính. Sau đó, nó sẽ tìm kiếm trình xử lý ngắt đã được đăng ký vớiattachInterrupt(), và nó sẽ gọi trình xử lý đó, đó là read_pin()chức năng của chúng tôi ở trên. Chức năng của chúng tôi sau đó sẽ gọi digitalRead()từ libcore. digitalRead()sẽ xem xét một số bảng để ánh xạ số cổng Arduino sang cổng I / O phần cứng mà nó phải đọc và số bit liên quan để kiểm tra. Nó cũng sẽ kiểm tra xem có một kênh PWM trên chân đó cần được vô hiệu hóa hay không. Sau đó nó sẽ đọc cổng I / O ... và chúng ta đã hoàn thành. Chà, chúng tôi không thực sự phục vụ ngắt, nhưng nhiệm vụ quan trọng về thời gian (đọc cổng I / O) đã được thực hiện và đó là tất cả vấn đề khi chúng tôi xem xét độ trễ.

Dưới đây là một bản tóm tắt ngắn gọn về tất cả những điều trên, cùng với sự chậm trễ liên quan trong chu kỳ CPU:

  1. trình tự cứng: hoàn thành hướng dẫn hiện tại, ngăn chặn các ngắt lồng nhau, lưu PC, địa chỉ tải của vectơ (4 chu kỳ)
  2. thực hiện véc tơ ngắt: nhảy tới ISR ​​(3 chu kỳ)
  3. Mở đầu ISR: lưu các thanh ghi (32 chu kỳ)
  4. Phần thân chính của ISR: xác định vị trí và gọi chức năng do người dùng đăng ký (13 chu kỳ)
  5. read_pin: gọi digitalRead (5 chu kỳ)
  6. digitalRead: tìm cổng và bit có liên quan để kiểm tra (41 chu kỳ)
  7. digitalRead: đọc cổng I / O (1 chu kỳ)

Chúng tôi sẽ giả sử trường hợp tốt nhất, với 4 chu kỳ cho chuỗi cứng. Điều này mang lại cho chúng tôi tổng độ trễ là 99 chu kỳ, tương đương khoảng 6,2 giây với xung nhịp 16 MHz. Sau đây, tôi sẽ khám phá một số thủ thuật có thể được sử dụng để giảm độ trễ này. Họ đến gần theo thứ tự phức tạp ngày càng tăng, nhưng tất cả họ đều cần chúng tôi bằng cách nào đó đào sâu vào nội bộ của MCU.

Sử dụng truy cập cổng trực tiếp

Mục tiêu đầu tiên rõ ràng để rút ngắn độ trễ là digitalRead(). Chức năng này cung cấp một sự trừu tượng hóa tốt cho phần cứng MCU, nhưng nó quá kém hiệu quả đối với công việc quan trọng về thời gian. Loại bỏ cái này thực sự không quan trọng: chúng ta chỉ cần thay thế nó bằng digitalReadFast(), từ thư viện digitalwritefast . Điều này giúp giảm độ trễ gần một nửa với chi phí tải xuống nhỏ!

Chà, điều đó quá dễ để trở thành niềm vui, tôi sẽ chỉ cho bạn cách làm điều đó một cách khó khăn. Mục đích là để chúng ta bắt đầu vào những thứ cấp thấp. Phương pháp này được gọi là " truy cập cổng trực tiếp " và được ghi lại độc đáo trên tài liệu tham khảo Arduino tại trang trên Thanh ghi cổng . Tại thời điểm này, bạn nên tải xuống và xem qua bảng dữ liệu ATmega328P . Tài liệu dài 650 trang này có vẻ hơi nản chí ngay từ cái nhìn đầu tiên. Tuy nhiên, nó được tổ chức tốt thành các phần dành riêng cho từng thiết bị ngoại vi và tính năng MCU. Và chúng ta chỉ cần kiểm tra các phần có liên quan đến những gì chúng ta đang làm. Trong trường hợp này, nó là phần tên I / O cổng . Dưới đây là một bản tóm tắt về những gì chúng ta học được từ những bài đọc đó:

  • Chân Arduino 2 thực sự được gọi là PD2 (tức là cổng D, bit 2) trên chip AVR.
  • Chúng tôi nhận được toàn bộ cổng D cùng một lúc bằng cách đọc một thanh ghi MCU đặc biệt gọi là "PIND".
  • Sau đó, chúng tôi kiểm tra bit số 2 bằng cách thực hiện logic bitwise và (toán tử C '&') với 1 << 2.

Vì vậy, đây là trình xử lý ngắt được sửa đổi của chúng tôi:

#define PIN_REG    PIND  // interrupt 0 is on AVR pin PD2
#define PIN_BIT    2

/* Interrupt handler. */
void read_pin()
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Bây giờ, trình xử lý của chúng tôi sẽ đọc thanh ghi I / O ngay khi được gọi. Độ trễ là 53 chu kỳ CPU. Thủ thuật đơn giản này đã giúp chúng tôi tiết kiệm được 46 chu kỳ!

Viết ISR của riêng bạn

Mục tiêu tiếp theo để cắt xén chu kỳ là ISR INT0_vect. ISR này là cần thiết để cung cấp chức năng của attachInterrupt(): chúng ta có thể thay đổi trình xử lý ngắt bất cứ lúc nào trong khi thực hiện chương trình. Tuy nhiên, mặc dù tốt đẹp để có, điều này không thực sự hữu ích cho mục đích của chúng tôi. Do đó, thay vì xác định vị trí ISR của libcore và gọi trình xử lý ngắt của chúng tôi, chúng tôi sẽ lưu một vài chu kỳ bằng cách thay thế ISR bằng trình xử lý của chúng tôi.

Điều này không khó như âm thanh. ISR có thể được viết như các hàm bình thường, chúng ta chỉ cần biết tên cụ thể của chúng và xác định chúng bằng cách sử dụng một ISR()macro đặc biệt từ avr-libc. Tại thời điểm này, sẽ tốt hơn nếu bạn xem tài liệu của avr-libc về các ngắt và tại phần biểu dữ liệu có tên là Ngắt bên ngoài . Dưới đây là tóm tắt ngắn gọn:

  • Chúng ta phải viết một bit trong một thanh ghi phần cứng đặc biệt gọi là EICRA (Thanh ghi điều khiển ngắt ngoài A ) để cấu hình ngắt được kích hoạt trên bất kỳ thay đổi nào của giá trị pin. Điều này sẽ được thực hiện trong setup().
  • Chúng ta phải viết một chút trong một thanh ghi phần cứng khác gọi là EIMSK ( thanh ghi Ma ngắt gián đoạn bên ngoài ) để kích hoạt ngắt INT0. Điều này cũng sẽ được thực hiện trong setup().
  • Chúng ta phải xác định ISR bằng cú pháp ISR(INT0_vect) { ... }.

Đây là mã cho ISR và setup(), mọi thứ khác đều không thay đổi:

/* Interrupt service routine for INT0. */
ISR(INT0_vect)
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

void setup()
{
    Serial.begin(9600);
    EICRA = 1 << ISC00;  // sense any change on the INT0 pin
    EIMSK = 1 << INT0;   // enable INT0 interrupt
}

Điều này đi kèm với một phần thưởng miễn phí: vì ISR này đơn giản hơn so với cái mà nó thay thế, nó cần ít đăng ký hơn để thực hiện công việc của mình, sau đó phần mở đầu tiết kiệm đăng ký ngắn hơn. Bây giờ chúng tôi xuống đến độ trễ 20 chu kỳ. Không tệ khi xem xét rằng chúng tôi đã bắt đầu gần 100!

Tại thời điểm này tôi sẽ nói rằng chúng ta đã hoàn thành. Nhiệm vụ đã hoàn thành. Những gì tiếp theo chỉ dành cho những người không sợ bị bẩn tay với một số lắp ráp AVR. Nếu không, bạn có thể dừng đọc ở đây, và cảm ơn bạn đã đi rất xa.

Viết một ISR trần trụi

Vẫn ở đây? Tốt Để tiến xa hơn, sẽ có ích khi có ít nhất một số ý tưởng rất cơ bản về cách thức hoạt động của lắp ráp và xem qua Sách hướng dẫn lắp ráp nội tuyến từ tài liệu avr-libc. Tại thời điểm này, trình tự nhập ngắt của chúng tôi trông như thế này:

  1. trình tự cứng (4 chu kỳ)
  2. vector ngắt: nhảy đến ISR (3 chu kỳ)
  3. Mở đầu ISR: lưu regs (12 chu kỳ)
  4. Điều đầu tiên trong phần thân ISR: đọc cổng IO (1 chu kỳ)

Nếu chúng ta muốn làm tốt hơn, chúng ta phải chuyển việc đọc cổng vào phần mở đầu. Ý tưởng là như sau: đọc thanh ghi PIND sẽ ghi đè một thanh ghi CPU, do đó chúng ta cần lưu ít nhất một thanh ghi trước khi thực hiện điều đó, nhưng các thanh ghi khác có thể đợi. Sau đó chúng ta cần viết một phần mở đầu tùy chỉnh đọc cổng I / O ngay sau khi lưu thanh ghi đầu tiên. Bạn đã thấy trong tài liệu ngắt avr-libc (bạn đã đọc rồi phải không?) Rằng một ISR có thể được thực hiện trần trụi , trong trường hợp đó, trình biên dịch sẽ không phát ra lời mở đầu hoặc đoạn kết, cho phép chúng ta viết phiên bản tùy chỉnh của riêng mình.

Vấn đề với cách tiếp cận này là có thể cuối cùng chúng ta sẽ viết toàn bộ ISR. Không phải là một vấn đề lớn, nhưng tôi muốn có trình biên dịch viết những lời mở đầu và tiểu thuyết nhàm chán cho tôi. Vì vậy, đây là mánh khóe bẩn thỉu: chúng tôi sẽ chia ISR thành hai phần:

  • phần đầu tiên sẽ là một đoạn lắp ráp ngắn
    • lưu một thanh ghi vào ngăn xếp
    • đọc PIND vào đăng ký đó
    • lưu trữ giá trị đó thành một biến toàn cục
    • khôi phục thanh ghi từ ngăn xếp
    • nhảy sang phần thứ hai
  • phần thứ hai sẽ là mã C thông thường với phần mở đầu và phần kết do trình biên dịch tạo

INT0 ISR trước đây của chúng tôi sau đó được thay thế bằng điều này:

volatile uint8_t sampled_pin;    // this is now a global variable

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    push r0                \n"  // save register r0
    "    in r0, %[pin]          \n"  // read PIND into r0
    "    sts sampled_pin, r0    \n"  // store r0 in a global
    "    pop r0                 \n"  // restore previous r0
    "    rjmp INT0_vect_part_2  \n"  // go to part 2
    :: [pin] "I" (_SFR_IO_ADDR(PIND)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Ở đây chúng tôi đang sử dụng macro ISR () để có công cụ biên dịch INT0_vect_part_2với phần mở đầu và đoạn kết cần thiết. Trình biên dịch sẽ phàn nàn rằng "'INT0_vect_part_2' dường như là một trình xử lý tín hiệu sai chính tả", nhưng cảnh báo có thể được bỏ qua một cách an toàn. Bây giờ ISR có một lệnh 2 chu kỳ duy nhất trước khi đọc cổng thực tế và tổng độ trễ chỉ là 10 chu kỳ.

Sử dụng thanh ghi GPIOR0

Điều gì xảy ra nếu chúng ta có thể có một đăng ký dành riêng cho công việc cụ thể này? Sau đó, chúng tôi sẽ không cần lưu bất cứ điều gì trước khi đọc cổng. Chúng ta thực sự có thể yêu cầu trình biên dịch liên kết một biến toàn cục với một thanh ghi . Tuy nhiên, điều này sẽ yêu cầu chúng tôi biên dịch lại toàn bộ lõi Arduino và libc để đảm bảo rằng thanh ghi luôn được bảo lưu. Không thực sự thuận tiện. Mặt khác, ATmega328P tình cờ có ba thanh ghi không được trình biên dịch cũng như bất kỳ thư viện nào sử dụng và có sẵn để lưu trữ bất cứ thứ gì chúng ta muốn. Chúng được gọi là GPIOR0, GPIOR1 và GPIOR2 (Thanh ghi I / O mục đích chung ). Mặc dù chúng được ánh xạ trong không gian địa chỉ I / O của MCU, nhưng thực tế chúng không phải làCác thanh ghi I / O: chúng chỉ là bộ nhớ đơn giản, giống như ba byte RAM bị mất trong một chiếc xe buýt và kết thúc ở không gian địa chỉ sai. Chúng không có khả năng như các thanh ghi CPU bên trong và chúng tôi không thể sao chép PIND vào một trong số này bằng inhướng dẫn. GPIOR0 rất thú vị, mặc dù, ở chỗ nó có địa chỉ bit , giống như PIND. Điều này sẽ cho phép chúng tôi chuyển thông tin mà không bị ghi đè bất kỳ thanh ghi CPU bên trong nào.

Đây là mẹo: chúng tôi sẽ đảm bảo rằng GPIOR0 ban đầu bằng 0 (nó thực sự bị xóa bởi phần cứng khi khởi động), sau đó chúng tôi sẽ sử dụng sbic(Bỏ qua hướng dẫn tiếp theo nếu một số Bit trong một số thanh ghi I / o bị xóa) và sbi( Đặt thành 1 một số Bit trong một số hướng dẫn đăng ký I / o) như sau:

sbic PIND, 2   ; skip the following if bit 2 of PIND is clear
sbi GPIOR0, 0  ; set to 1 bit 0 of GPIOR0

Bằng cách này, GPIOR0 sẽ kết thúc bằng 0 hoặc 1 tùy thuộc vào bit chúng tôi muốn đọc từ PIND. Lệnh sbic mất 1 hoặc 2 chu kỳ để thực thi tùy thuộc vào điều kiện là sai hay đúng. Rõ ràng, bit PIND được truy cập trong chu kỳ đầu tiên. Trong phiên bản mã mới này, biến toàn cục sampled_pinkhông còn hữu ích nữa, vì về cơ bản nó được thay thế bằng GPIOR0:

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    sbic %[pin], %[bit]    \n"
    "    sbi %[gpio], 0         \n"
    "    rjmp INT0_vect_part_2  \n"
    :: [pin]  "I" (_SFR_IO_ADDR(PIND)),
       [bit]  "I" (PIN_BIT),
       [gpio] "I" (_SFR_IO_ADDR(GPIOR0)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

Cần lưu ý rằng GPIOR0 phải luôn được đặt lại trong ISR.

Bây giờ, việc lấy mẫu thanh ghi I / O PIND là điều đầu tiên được thực hiện bên trong ISR. Tổng độ trễ là 8 chu kỳ. Đây là về những điều tốt nhất chúng ta có thể làm trước khi bị vấy bẩn với những cặn bã tội lỗi khủng khiếp. Đây lại là một cơ hội tốt để ngừng đọc ...

Đặt mã thời gian quan trọng trong bảng vectơ

Đối với những người vẫn còn ở đây, đây là tình hình hiện tại của chúng tôi:

  1. trình tự cứng (4 chu kỳ)
  2. vector ngắt: nhảy đến ISR (3 chu kỳ)
  3. Phần thân ISR: đọc cổng IO (trên chu kỳ 1)

Rõ ràng là có rất ít phòng để cải thiện. Cách duy nhất chúng ta có thể rút ngắn độ trễ tại thời điểm này là bằng cách thay thế chính vectơ ngắt bằng mã của chúng ta. Được cảnh báo rằng điều này sẽ vô cùng khó chịu với bất cứ ai coi trọng thiết kế phần mềm sạch. Nhưng nó có thể, và tôi sẽ chỉ cho bạn cách.

Bố cục của bảng vectơ ATmega328P có thể được tìm thấy trong biểu dữ liệu, phần Ngắt , phần phụ ngắt trong ATmega328 và ATmega328P . Hoặc bằng cách phân tách bất kỳ chương trình nào cho chip này. Đây là cách nó trông như thế nào. Tôi đang sử dụng các quy ước của avr-gcc và avr-libc (__init là vectơ 0, địa chỉ được tính bằng byte) khác với Atmel's.

address  instruction      comment
────────┼─────────────────┼──────────────────────
 0x0000  jmp __init       reset vector 
 0x0004  jmp __vector_1   a.k.a. INT0_vect
 0x0008  jmp __vector_2   a.k.a. INT1_vect
 0x000c  jmp __vector_3   a.k.a. PCINT0_vect
  ...
 0x0064  jmp __vector_25  a.k.a. SPM_READY_vect

Mỗi vector có một khe 4 byte, chứa đầy một jmplệnh. Đây là một lệnh 32 bit, không giống như hầu hết các lệnh AVR là 16 bit. Nhưng một khe 32 bit quá nhỏ để giữ phần đầu tiên của ISR của chúng tôi: chúng tôi có thể phù hợp với các hướng dẫn sbicsbi, nhưng không phải là rjmp. Nếu chúng ta làm điều đó, bảng vector kết thúc giống như thế này:

address  instruction      comment
────────┼─────────────────┼──────────────────────
 0x0000  jmp __init       reset vector 
 0x0004  sbic PIND, 2     the first part...
 0x0006  sbi GPIOR0, 0    ...of our ISR
 0x0008  jmp __vector_2   a.k.a. INT1_vect
 0x000c  jmp __vector_3   a.k.a. PCINT0_vect
  ...
 0x0064  jmp __vector_25  a.k.a. SPM_READY_vect

Khi INT0 kích hoạt, PIND sẽ được đọc, bit có liên quan sẽ được sao chép vào GPIOR0, và sau đó việc thực thi sẽ chuyển sang vectơ tiếp theo. Sau đó, ISR cho INT1 sẽ được gọi, thay vì ISR cho INT0. Điều này thật đáng sợ, nhưng vì dù sao chúng tôi không sử dụng INT1, nên chúng tôi sẽ chỉ "chiếm đoạt" vectơ của nó để phục vụ INT0.

Bây giờ, chúng ta chỉ cần viết bảng vectơ tùy chỉnh của riêng mình để ghi đè lên bảng mặc định. Hóa ra nó không dễ dàng như vậy. Bảng vectơ mặc định được cung cấp bởi phân phối avr-libc, trong tệp đối tượng có tên crtm328p.o được liên kết tự động với bất kỳ chương trình nào chúng tôi xây dựng. Không giống như mã thư viện, mã tệp đối tượng không có nghĩa là bị ghi đè: cố gắng làm điều đó sẽ gây ra lỗi liên kết về bảng được xác định hai lần. Điều này có nghĩa là chúng tôi phải thay thế toàn bộ crtm328p.o bằng phiên bản tùy chỉnh của chúng tôi. Một tùy chọn là tải xuống mã nguồn avr-libc đầy đủ , thực hiện các sửa đổi tùy chỉnh của chúng tôi trong gcrt1.S , sau đó xây dựng mã này dưới dạng libc tùy chỉnh.

Ở đây tôi đã đi cho một cách tiếp cận nhẹ hơn, thay thế. Tôi đã viết một crt.S tùy chỉnh, đây là phiên bản đơn giản hóa của bản gốc từ avr-libc. Nó thiếu một vài tính năng hiếm khi được sử dụng, như khả năng xác định ISR "bắt tất cả" hoặc có thể chấm dứt chương trình (tức là đóng băng Arduino) bằng cách gọi exit(). Đây là mã. Tôi đã cắt phần lặp đi lặp lại của bảng vectơ để giảm thiểu việc cuộn:

#include <avr/io.h>

.weak __heap_end
.set  __heap_end, 0

.macro vector name
    .weak \name
    .set \name, __vectors
    jmp \name
.endm

.section .vectors
__vectors:
    jmp __init
    sbic _SFR_IO_ADDR(PIND), 2   ; these 2 lines...
    sbi _SFR_IO_ADDR(GPIOR0), 0  ; ...replace vector_1
    vector __vector_2
    vector __vector_3
    [...and so forth until...]
    vector __vector_25

.section .init2
__init:
    clr r1
    out _SFR_IO_ADDR(SREG), r1
    ldi r28, lo8(RAMEND)
    ldi r29, hi8(RAMEND)
    out _SFR_IO_ADDR(SPL), r28
    out _SFR_IO_ADDR(SPH), r29

.section .init9
    jmp main

Nó có thể được biên dịch với dòng lệnh sau:

avr-gcc -c -mmcu=atmega328p silly-crt.S

Bản phác thảo giống hệt bản trước ngoại trừ không có INT0_vect và INT0_vect_part_2 được thay thế bằng INT1_vect:

/* Interrupt service routine for INT1 hijacked to service INT0. */
ISR(INT1_vect)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

Để biên dịch bản phác thảo, chúng ta cần một lệnh biên dịch tùy chỉnh. Nếu bạn đã theo dõi cho đến nay, bạn có thể biết cách biên dịch từ dòng lệnh. Bạn phải yêu cầu silly-crt.o một cách rõ ràng để được liên kết với chương trình của bạn và thêm -nostartfilestùy chọn để tránh liên kết trong crtm328p.o ban đầu.

Bây giờ, việc đọc cổng I / O là lệnh đầu tiên được thực hiện sau khi kích hoạt ngắt. Tôi đã thử nghiệm phiên bản này bằng cách gửi cho nó các xung ngắn từ một Arduino khác và nó có thể bắt được (mặc dù không đáng tin cậy) mức xung cao chỉ trong 5 chu kỳ. Không có gì hơn chúng ta có thể làm để rút ngắn độ trễ ngắt trên phần cứng này.


2
Lời giải thích hay! +1
Nick Gammon

6

Ngắt đang được thiết lập để kích hoạt thay đổi và test_func của bạn được đặt là Định tuyến dịch vụ ngắt (ISR), được gọi là dịch vụ ngắt. ISR sau đó in giá trị của đầu vào.

Thoạt nhìn, bạn sẽ mong đợi đầu ra sẽ như bạn đã nói, và xen kẽ các mức thấp cao, vì nó chỉ đạt được ISR khi thay đổi.

Nhưng điều chúng ta thiếu là có một khoảng thời gian nhất định để CPU phục vụ ngắt và phân nhánh cho ISR. Trong thời gian này, điện áp trên pin có thể đã thay đổi một lần nữa. Đặc biệt nếu pin không được ổn định bằng cách tháo phần cứng hoặc tương tự. Do ngắt đã được gắn cờ và chưa được phục vụ, nên thay đổi bổ sung này (hoặc nhiều trong số chúng, vì mức chân có thể thay đổi rất nhanh so với tốc độ đồng hồ nếu nó có điện dung ký sinh thấp) sẽ bị bỏ qua.

Vì vậy, về bản chất mà không có một hình thức nào đó, chúng tôi không đảm bảo rằng khi đầu vào thay đổi và ngắt được đánh dấu để phục vụ, thì đầu vào vẫn sẽ ở cùng giá trị khi chúng ta đọc giá trị của nó trong ISR.

Như một ví dụ chung, Bảng dữ liệu ATmega328 được sử dụng trên Arduino Uno chi tiết thời gian gián đoạn trong phần 6.7.1 - "Thời gian đáp ứng ngắt". Nó tuyên bố cho bộ vi điều khiển này thời gian tối thiểu để phân nhánh tới ISR ​​để phục vụ là 4 chu kỳ xung nhịp, nhưng có thể nhiều hơn (thêm nếu thực hiện lệnh đa chu kỳ khi bị gián đoạn hoặc 8 + thời gian đánh thức giấc ngủ nếu MCU đang ngủ.)

Như @EdgarBonet đã đề cập trong các bình luận, pin cũng có thể thay đổi tương tự trong quá trình thực thi ISR. Vì ISR đọc từ pin hai lần, nên nó sẽ không thêm bất cứ thứ gì vào test_array nếu nó gặp THẤP ở lần đọc đầu tiên và CAO ở lần thứ hai. Nhưng x vẫn sẽ tăng, khiến cho vị trí đó trong mảng không thay đổi (có thể là dữ liệu chưa được khởi tạo tùy thuộc vào những gì đã được thực hiện cho mảng trước đó).

ISR một dòng của anh ấy test_array[x++] = digitalRead(pin);là một giải pháp hoàn hảo cho việc này.

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.