Quy trình dịch vụ ngắt của AVR không thực thi nhanh như mong đợi (phí chỉ dẫn?)


8

Tôi đang phát triển một bộ phân tích logic nhỏ với 7 đầu vào. Thiết bị mục tiêu của tôi là một ATmega168tốc độ xung nhịp 20 MHz. Để phát hiện các thay đổi logic, tôi sử dụng các ngắt thay đổi pin. Bây giờ tôi đang cố gắng tìm ra tỷ lệ mẫu thấp nhất tôi có thể phát hiện ra những thay đổi pin này. Tôi đã xác định giá trị tối thiểu 5,6 cụm (178,5 kHz). Mỗi tín hiệu dưới mức này tôi không thể nắm bắt chính xác.

Mã của tôi được viết bằng C (avr-gcc). Thói quen của tôi trông như:

ISR()
{
    pinc = PINC; // char
    timestamp_ll = TCNT1L; // char
    timestamp_lh = TCNT1H; // char
    timestamp_h = timerh; // 2 byte integer
    stack_counter++;
}

Thay đổi tín hiệu bắt được của tôi nằm ở pinc. Để bản địa hóa nó tôi có một giá trị dấu thời gian dài 4 byte.

Trong biểu dữ liệu tôi đọc thói quen dịch vụ ngắt có 5 đồng hồ để nhảy vào và 5 đồng hồ để trở về quy trình chính. Tôi giả sử mỗi lệnh trong tôi ISR()đang thực hiện 1 đồng hồ; Vì vậy, trong tổng số nên có một chi phí chung của 5 + 5 + 5 = 15đồng hồ. Thời lượng của một đồng hồ phải theo tốc độ xung nhịp là 20 MHz 1/20000000 = 0.00000005 = 50 ns. Tổng chi phí tính bằng giây sẽ là : 15 * 50 ns = 750 ns = 0.75 µs. Bây giờ tôi không hiểu tại sao tôi không thể chụp bất cứ thứ gì dưới 5,6. Bất cứ ai có thể giải thích những gì đang xảy ra?


có thể 5 đồng hồ để gửi mã ISR, bao gồm lưu bối cảnh và khôi phục epilog / prolog mà bạn không thấy trong nguồn C. Ngoài ra, phần cứng đang làm gì khi ngắt xảy ra? Có phải trong một số trạng thái ngủ. (Tôi không biết AVR, nhưng nói chung, việc gián đoạn quá trình xử lý một số trạng thái có thể mất nhiều thời gian hơn.)
Kaz

@arminb Xem thêm câu hỏi này để biết thêm ý tưởng về cách nắm bắt các sự kiện bên ngoài với độ chính xác cao hơn. Ngoài ra [chú thích này] (www.atmel.com/Images/doc2505.pdf) có thể được quan tâm.
angelatlarge

Câu trả lời:


10

Có một vài vấn đề:

  • Không phải tất cả các lệnh AVR đều mất 1 đồng hồ để được thực thi: nếu bạn nhìn vào mặt sau của biểu dữ liệu, nó có số lượng đồng hồ cần cho mỗi lệnh được thực thi. Vì vậy, ví dụ ANDlà một lệnh một đồng hồ, MUL(nhân) lấy hai đồng hồ, trong khi LPM(bộ nhớ chương trình tải) là ba và CALLlà 4. Vì vậy, đối với việc thực hiện lệnh, nó thực sự phụ thuộc vào lệnh.
  • 5 đồng hồ để nhảy vào và 5 đồng hồ để trở lại có thể gây hiểu nhầm. Nếu bạn nhìn vào mã được tháo rời của mình, bạn sẽ thấy rằng ngoài bước nhảy và RETIhướng dẫn, trình biên dịch sẽ thêm tất cả các loại mã khác, cũng mất thời gian. Ví dụ, bạn có thể cần các biến cục bộ được tạo trên ngăn xếp và phải được bật ra, v.v. Điều tốt nhất để làm những gì thực sự đang diễn ra là xem xét việc tháo gỡ.
  • Cuối cùng, hãy nhớ rằng trong khi bạn đang trong thói quen ISR, các ngắt của bạn không được kích hoạt. Điều này có nghĩa là bạn sẽ không thể có được loại hiệu suất mà bạn đang tìm kiếm từ bộ phân tích logic của mình, trừ khi bạn biết rằng mức tín hiệu của bạn thay đổi trong khoảng thời gian dài hơn thời gian phục vụ ngắt của bạn. Để rõ ràng, một khi bạn tính toán thời gian để ISR của bạn thực thi, điều này mang lại cho bạn giới hạn trên về mức độ bạn có thể bắt được một tín hiệu . Nếu bạn cần nắm bắt hai tín hiệu, thì bạn bắt đầu gặp rắc rối. Để được chi tiết quá mức về điều này, hãy xem xét kịch bản sau đây:

nhập mô tả hình ảnh ở đây

Nếu xlà thời gian cần thiết để phục vụ ngắt của bạn, thì tín hiệu B sẽ không bao giờ bị bắt.


Nếu chúng tôi lấy mã ISR của bạn, hãy gắn mã đó vào thói quen ISR (tôi đã sử dụng ISR(PCINT0_vect)), khai báo tất cả các biến volatilevà biên dịch cho ATmega168P, mã được phân tách trông như sau (xem câu trả lời của @ jíp để biết thêm thông tin) trước khi chúng tôi nhận được mã rằng "làm một cái gì đó" ; nói cách khác, phần mở đầu cho ISR của bạn như sau:

  37                    .loc 1 71 0
  38                    .cfi_startproc
  39 0000 1F92              push r1
  40                .LCFI0:
  41                    .cfi_def_cfa_offset 3
  42                    .cfi_offset 1, -2
  43 0002 0F92              push r0
  44                .LCFI1:
  45                    .cfi_def_cfa_offset 4
  46                    .cfi_offset 0, -3
  47 0004 0FB6              in r0,__SREG__
  48 0006 0F92              push r0
  49 0008 1124              clr __zero_reg__
  50 000a 8F93              push r24
  51                .LCFI2:
  52                    .cfi_def_cfa_offset 5
  53                    .cfi_offset 24, -4
  54 000c 9F93              push r25
  55                .LCFI3:
  56                    .cfi_def_cfa_offset 6
  57                    .cfi_offset 25, -5
  58                /* prologue: Signal */
  59                /* frame size = 0 */
  60                /* stack size = 5 */
  61                .L__stack_usage = 5

vì vậy, PUSHx 5, inx 1, clrx 1. Không tệ như bình 32 bit của jíp, nhưng vẫn không có gì.

Một số điều này là thận trọng (mở rộng các cuộc thảo luận trong các ý kiến). Rõ ràng, vì thói quen ISR có thể xảy ra bất cứ lúc nào, nên nó phải dự đoán các thanh ghi mà nó sử dụng, trừ khi bạn biết rằng không có mã nào xảy ra gián đoạn sử dụng cùng một thanh ghi như thói quen ngắt của bạn. Ví dụ: dòng sau trong ISR đã phân tách:

push r24

Có phải bởi vì mọi thứ đều đi qua r24: của bạn pincđược tải ở đó trước khi nó đi vào bộ nhớ, v.v. Vì vậy, bạn phải có nó trước. __SREG__được tải vào r0và sau đó được đẩy: nếu điều này có thể đi qua r24thì bạn có thể tự cứu mìnhPUSH


Một số giải pháp có thể:

  • Sử dụng một vòng bỏ phiếu chặt chẽ theo đề xuất của Kaz trong các bình luận. Đây có lẽ sẽ là giải pháp nhanh nhất, cho dù bạn viết vòng lặp bằng C hay lắp ráp.
  • Viết ISR của bạn trong hội đồng: theo cách này bạn có thể tối ưu hóa việc sử dụng đăng ký theo cách mà số lượng ít nhất trong số chúng cần được lưu trong ISR.
  • Khai báo các thói quen ISR của bạn ISR_NAKED , mặc dù điều này hóa ra là một giải pháp cá trích đỏ nhiều hơn. Khi bạn khai báo các thói quen ISR ISR_NAKED, gcc không tạo mã prologue / epilogue và bạn có trách nhiệm lưu bất kỳ đăng ký nào mà mã của bạn sửa đổi, cũng như gọi reti(trả về từ một ngắt). Thật không may, không có cách nào sử dụng các thanh ghi trong avr-gcc C trực tiếp (rõ ràng bạn có thể lắp ráp), tuy nhiên, điều bạn có thể làm là liên kết các biến với các thanh ghi cụ thể với các từ khóa register+ asm, như sau : register uint8_t counter asm("r3");. Nếu bạn làm điều đó, đối với ISR, bạn sẽ biết những gì bạn đang sử dụng đăng ký trong ISR. Vấn đề sau đó là không có cách nào để tạo pushpopđể lưu các thanh ghi được sử dụng mà không cần lắp ráp nội tuyến (xem điểm 1). Để đảm bảo phải lưu ít thanh ghi hơn, bạn cũng có thể liên kết tất cả các biến không phải ISR ​​với các thanh ghi cụ thể, tuy nhiên, không có vấn đề gì mà gcc sử dụng các thanh ghi để xáo trộn dữ liệu đến và từ bộ nhớ. Điều này có nghĩa là trừ khi bạn nhìn vào quá trình tháo gỡ, bạn sẽ không biết đăng ký sử dụng mã chính nào. Vì vậy, nếu bạn đang xem xét ISR_NAKED, bạn cũng có thể viết ISR trong hội đồng.

Cảm ơn, vì vậy mã C của tôi làm cho chi phí rất lớn? Nó sẽ nhanh hơn nếu tôi viết nó trong trình biên dịch chương trình? Về điều thứ hai, tôi đã nhận thức được điều đó.
arminb

@arminb: Tôi không biết đủ để trả lời câu hỏi đó. Giả định của tôi sẽ là trình biên dịch hợp lý thông minh, và nó làm những gì nó làm vì một lý do. Phải nói rằng tôi chắc chắn rằng nếu bạn dành một chút thời gian cho việc lắp ráp, bạn có thể vắt thêm vài chu kỳ đồng hồ ra khỏi thói quen ISR của mình.
angelatlarge

1
Tôi nghĩ rằng nếu bạn muốn phản hồi nhanh nhất, bạn thường tránh bị gián đoạn và thăm dò các chân trong một vòng lặp chặt chẽ.
Kaz

1
Với các mục tiêu cụ thể trong tâm trí, có thể tối ưu hóa mã bằng cách sử dụng trình biên dịch chương trình. Ví dụ, trình biên dịch bắt đầu với việc đẩy tất cả các thanh ghi được sử dụng lên stack, sau đó bắt đầu thực hiện các thường trình thực tế. Nếu bạn có thời gian quan trọng, bạn có thể di chuyển một số công cụ đẩy lùi và kéo thời gian quan trọng về phía trước. Vì vậy, có bạn có thể tối ưu hóa bằng cách sử dụng trình biên dịch chương trình, nhưng bản thân trình biên dịch cũng khá thông minh. Tôi thích sử dụng mã được biên dịch làm điểm bắt đầu và sửa đổi thủ công cho các yêu cầu cụ thể của mình.
jippie

1
Câu trả lời thực sự tốt đẹp. Tôi sẽ thêm rằng trình biên dịch thêm tất cả các loại lưu trữ và lưu trữ đăng ký để phù hợp với hầu hết các nhu cầu của người dùng. Có thể viết trình xử lý ngắt xương trần của riêng bạn-- nếu bạn không cần tất cả chi phí đó. Một số trình biên dịch thậm chí có thể cung cấp một tùy chọn để tạo ra một ngắt "nhanh", để lại phần lớn "sổ sách kế toán" cho lập trình viên. Tôi không nhất thiết phải đi đúng vòng lặp không có ISR nếu tôi không thể đáp ứng lịch trình của mình. Đầu tiên tôi sẽ xem xét một uC nhanh hơn, và sau đó tôi sẽ tìm hiểu xem liệu tôi có thể sử dụng một số loại phần cứng keo, như chốt và RTC không.
Scott Seidman

2

Có rất nhiều thanh ghi PUSH và POP để xếp chồng lên nhau trước khi ISR ​​thực tế của bạn bắt đầu, đó là trên cùng của 5 chu kỳ đồng hồ bạn đề cập. Hãy xem việc tháo gỡ mã được tạo.

Tùy thuộc vào chuỗi công cụ bạn sử dụng, việc hủy bỏ hội đồng liệt kê chúng tôi đã thực hiện theo nhiều cách khác nhau. Tôi làm việc trên dòng lệnh Linux và đây là lệnh tôi sử dụng (nó yêu cầu tệp .elf làm đầu vào):

avr-objdump -C -d $(src).elf

Hãy xem một sniplet mã mà tôi mới sử dụng cho một ATtiny. Đây là những gì mã C trông giống như:

ISR( INT0_vect ) {
        uint8_t myTIFR  = TIFR;
        uint8_t myTCNT1 = TCNT1;

Và đây là mã lắp ráp được tạo cho nó:

00000056 <INT0_vect>:
  56:   1f 92           push    r1
  58:   0f 92           push    r0
  5a:   0f b6           in      r0, SREG        ; 0x3f
  5c:   0f 92           push    r0
  5e:   11 24           eor     r1, r1
  60:   2f 93           push    r18
  62:   3f 93           push    r19
  64:   4f 93           push    r20
  66:   8f 93           push    r24
  68:   9f 93           push    r25
  6a:   af 93           push    r26
  6c:   bf 93           push    r27
  6e:   48 b7           in      r20, TIFR       ; uint8_t myTIFR  = TIFR;
  70:   2f b5           in      r18, TCNT1      ; uint8_t myTCNT1 = TCNT1;

Thành thật mà nói, thói quen C của tôi sử dụng thêm một vài biến số gây ra tất cả các lần đẩy và pop này, nhưng bạn hiểu ý.

Đang tải một biến 32 bit trông như thế này:

  ec:   80 91 78 00     lds     r24, 0x0078
  f0:   90 91 79 00     lds     r25, 0x0079
  f4:   a0 91 7a 00     lds     r26, 0x007A
  f8:   b0 91 7b 00     lds     r27, 0x007B

Việc tăng biến 32 bit lên 1 trông như thế này:

  5e:   11 24           eor     r1, r1
  d6:   01 96           adiw    r24, 0x01       ; 1
  d8:   a1 1d           adc     r26, r1
  da:   b1 1d           adc     r27, r1

Lưu trữ một biến 32 bit trông như thế này:

  dc:   80 93 78 00     sts     0x0078, r24
  e0:   90 93 79 00     sts     0x0079, r25
  e4:   a0 93 7a 00     sts     0x007A, r26
  e8:   b0 93 7b 00     sts     0x007B, r27

Sau đó, tất nhiên bạn phải bật các giá trị cũ khi bạn rời khỏi ISR:

 126:   bf 91           pop     r27
 128:   af 91           pop     r26
 12a:   9f 91           pop     r25
 12c:   8f 91           pop     r24
 12e:   4f 91           pop     r20
 130:   3f 91           pop     r19
 132:   2f 91           pop     r18
 134:   0f 90           pop     r0
 136:   0f be           out     SREG, r0        ; 0x3f
 138:   0f 90           pop     r0
 13a:   1f 90           pop     r1
 13c:   18 95           reti

Theo tóm tắt hướng dẫn trong biểu dữ liệu, hầu hết các hướng dẫn là chu kỳ đơn, nhưng PUSH và POP là chu kỳ kép. Bạn có ý tưởng về sự chậm trễ đến từ đâu?


Cảm ơn câu trả lời của bạn! Bây giờ tôi biết về những gì đang xảy ra. Đặc biệt cảm ơn bạn đã ra lệnh avr-objdump -C -d $(src).elf!
arminb

Hãy dành một chút thời gian để hiểu các hướng dẫn lắp ráp avr-objdumpphun ra, chúng được giải thích ngắn gọn trong biểu dữ liệu dưới Tóm tắt hướng dẫn. Theo tôi, đó là một cách thực hành tốt để làm quen với việc ghi nhớ vì nó có thể giúp ích rất nhiều khi gỡ lỗi mã C của bạn.
jippie

Trên thực tế, việc phân tách rất hữu ích khi là một phần của mặc định của bạn Makefile: vì vậy, bất cứ khi nào bạn xây dựng dự án của mình, nó cũng được tự động tháo rời để bạn không phải suy nghĩ về nó hoặc nhớ cách thực hiện thủ công.
angelatlarge
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.