Bài 3 - Ngắt ngoài

( 161 Votes )

Nội dung Các bài cần tham khảo trước
  1. Ngắt trên AVR.
  2. Ngắt ngoài.
  3. Ví dụ ngắt ngoài với C.
Download ví dụ

 

I. Ngắt trên AVR.

     Interrupts, thường được gọi là ngắt, là một tín hiệu khẩn cấp gởi đến bộ xử lí, yêu cầu bộ xử lí tạm ngừng tức khắc các hoạt động hiện tại để “nhảy” đến một nơi khác thực hiện một nhiệm vụ khẩn cấp nào đó, nhiệm vụ này gọi là trình phục vụ ngắt – isr (interrupt service routine ). Sau khi kết thúc nhiệm vụ trong isr, bộ đếm chương trình sẽ được trả về giá trị trước đó để bộ xử lí quay về thực hiện tiếp các nhiệm vụ còn dang dở. Như vậy, ngắt có mức độ ưu tiên xử lí cao nhất, ngắt thường được dùng để xử lí các sự kiện bất ngờ nhưng không tốn quá nhiều thời gian. Các tín hiệu dẫn đến ngắt có thể xuất phát từ các thiết bị bên trong chip (ngắt báo bộ đếm timer/counter tràn, ngắt báo quá trình gởi dữ liệu bằng RS232 kết thúc…) hay do các tác nhân bên ngoài (ngắt báo có 1 button được nhấn, ngắt báo có 1 gói dữ liệu đã được nhận…).

     Ngắt là một trong 2 kỹ thuật “bắt” sự kiện cơ bản là hỏi vòng (Polling) và ngắt. Hãy tưởng tượng bạn cần thiết kế một mạch điều khiển hoàn chỉnh thực hiện rất nhiều nhiệm vụ bao gồm nhận thông tin từ người dùng qua các button hay keypad (hoặc keyboard), nhận tín hiệu từ cảm biến, xử lí thông tin, xuất tín hiệu điều khiển, hiển thị thông tin trạng thái lên các LCD…(bạn hoàn toàn có thể làm được với AVR), rõ ràng trong các nhiệm vụ này việc nhận thông tin người dùng (start, stop, setup, change,…) rất hiếm xảy ra (so với các nhiệm vụ khác) nhưng lại rất “khẩn cấp”, được ưu tiên hàng đầu. Nếu dùng Polling nghĩa là bạn cần viết 1 đoạn chương trình chuyên thăm dò trạng thái của các button (tôi tạm gọi đoạn chương trình đó là Input()) và bạn phải chèn đoạn chương trình Input() này vào rất nhiều vị trí trong chương trình chính để tránh trường hợp bỏ sót lệnh từ người dùng, điều này thật lãng phí thời gian thực thi. Giải pháp cho vấn đề này là sử dụng ngắt, bằng cách kết nối các button với đường ngắt của chip và sử dụng chương trình Input() làm trình phục vụ ngắt - isr của ngắt đó, bạn không cần phải chèn Input() trong lúc đang thực thi và vì thế không tốn thời gian cho nó, Input() chỉ được gọi khi người dùng nhấn các button. Đó là ý tưởng sử dụng ngắt.

     Hình 1 minh họa cách tổ chức ngắt thông thường trong các chip AVR. Số lượng ngắt trên mỗi dòng chip là khác nhau, ứng với mỗi ngắt sẽ có vector ngắt, vector ngắt là các thanh ghi có địa chỉ cố định được định nghĩa trước nằm trong phần đầu của bộ nhớ chương trình. Ví dụ vector ngắt ngoài 0 (external interrupt 0) của chip atmega8 có địa chỉ là 0x001 (theo datasheet từ Atmel). Trong lúc chương trình chính đang thực thi, nếu có một sự thay đổi dẫn đến ngắt xảy ra ở chân INT0 (chân 4), bộ đếm chương trình (Program Counter) nhảy đến địa chỉ 0x001, giả sử ngay tại địa chỉ 0x001 chúng ta có đặt 1 lệnh RJMP đến một trình phục vụ ngắt (IRS1 chẳng hạn), một lần nữa bộ đếm chương trình nhảy đến IRS1 để thực thi trình phục vụ ngắt, kết thúc ISR1, bộ đếm chương trình lại quay về vị trí trước đó trong chương trình chính, quá trình ngắt kết thúc. Không mang tính bắt buộc nhưng tôi khuyên bạn nên tổ chức chương trình ngắt theo cách này để tránh những lỗi liên quan đến địa chỉ chương trình.

ngắt

Hình 1. Ngắt.

     Bảng 1 tóm tắt các vector ngắt có trên chip atmega8, cho các chip khác bạn hãy tham khảo datasheet để biết thêm.

Bảng 1 các vector ngắt và Reset trên chip Atmega8.

ngắt

II. Ngắt ngoài (External Interrupt).

      Phần này tôi dành giới thiệu các bạn cách cài đặt và sử dụng ngắt ngoài vì đây là loại ngắt duy nhất độc lập với các thiết bị của chip, các ngắt khác thường gắn với hoạt động của 1 thiết bị nào đó như Timer/Counter, giao tiếp nối tiếp USART, chuyển đổi ADC…chúng ta sẽ khảo sát cụ thể khi tìm hiểu về hoạt động của các thiết bị này.

      Ngắt ngoài là cách rất hiệu quả để thực hiện giao tiếp giữa người dùng và chip. Trên chip atmega8 có 2 ngắt ngoài có tên là INT0 và INT1 tương ứng 2 chân số 4 (PD2) và số 5 (PD3). Như tôi đã đề cập trong bài AVR2, khi làm việc với các thiết bị ngoại vi của AVR, hầu như chúng ta chỉ thao tác trên các thanh ghi chức năng đặc biệt - SFR (Special Function Registers) trên vùng nhớ IO, mỗi thiết bị bao gồm một tập hợp các thanh ghi điều khiển, trạng thái, ngắt…khác nhau, điều này đồng nghĩa chúng ta phải nhớ tất cả các thanh ghi của AVR. Lúc này datasheet phát huy tác dụng, bạn phải nhanh chóng download file datasheet của chip mình đang sử dụng, có rất nhiều nơi để download như tại www.atmel.com hay trên các trang web chuyên cung cấp IC datasheet miễn phí (www.alldatasheet.com là 1 ví dụ). Quay về với ngắt ngoài, có 3 thanh ghi liên quan đến ngắt ngoài đó là MCUCR, GICR và GIFR. Cụ thể các thanh ghi được trình bày bên dưới.

      Thanh ghi điều khiển MCU – MCUCR (MCU Control Register) là thanh ghi xác lập chế độ ngắt cho ngắt ngoài, quan sát hình 2 trước khi tìm hiểu thanh ghi này.

connect

Hình 2. Kết nối ngắt ngoài cho atmega8.

      Giả sử chúng ta kết nối các ngắt ngoài trên AVR mega8 như phía trái hình 2, các button dùng tạo ra các ngắt. Có 4 khả năng (tạm gọi là các MODES) có thể xảy ra khi chúng ta nhấn và thả các button. Nếu không nhấn, trạng thái các chân INT là HIGH do điện trở kéo lên, khi vừa nhấn 1 button, sẽ có chuyển trạng thái từ HIGH sang LOW, chúng ta gọi là cạnh xuống - Falling Edge, khi button được nhấn và giữ, trạng thái các chân INT được xác định là LOW và cuối cùng khi thả các button, trạng thái chuyển từ LOW sang HIGH, gọi là cạnh lên – Rising Edge.  Trong những trường hợp cụ thể, 1 trong 4 MODES trên đều hữu ích, ví dụ trong các ứng dụng đếm xung (đếm encoder của servo motor chẳng hạn) thì 2 MODE “cạnh” phải được dùng. Thanh ghi MCUCR chứa các bits cho phép chúng ta chọn 1 trong 4 MODE trên cho các ngắt ngoài. Dưới đây là cấu trúc thanh ghi MCUCR được trích ra từ datasheet của chip atmega8.

MCUCR

      MCUCR là một thanh ghi 8 bit nhưng đối với hoạt động ngắt ngoài, chúng ta chỉ quan tâm đến 4 bit thấp của nó (4 bit cao dùng cho Power manager và Sleep Mode). Bốn bit thấp là các bit Interrupt Sense Control (ISC) trong đó 2 bit ISC11:ISC10 dùng cho INT1 và 2 bit ISC01:ISC00 dùng cho INT0. Hãy nhìn vào bảng tóm tắt bên dưới để biết chức năng của các bit trên, đây là bảng “chân trị” của 2 bit ISC11, ISC10. Bảng chân trị cho các bit ISC01, ISC00 hoàn toàn tương tự.

Bảng 2: INT1 Sense Control

Sense control

      Thật dễ dàng để hiểu chức năng của các bit Sense Control, ví dụ bạn muốn set cho INT1 là ngắt cạnh xuống (Falling Edge) trong khi INT0 là ngắt cạnh lên (Rising Edge), hãy đặt dòng lệnh MCUCR =0x0B (0x0B = 00001011 nhị phân) trong chương trình của bạn.

      Thanh ghi điều khiển ngắt chung – GICR (General Interrupt Control Register) (chú ý trên các chip AVR cũ, như các chip AT90Sxxxx, thanh ghi này có tên là thanh ghi mặt nạ ngắt thông thường GIMSK, bạn tham khảo thêm datasheet của các chip này nếu cần sử dụng đến). GICR cũng là 1 thanh ghi 8 bit nhưng chỉ có 2 bit cao (bit 6 và bit 7) là được sử dụng cho điều khiển ngắt, cấu trúc thanh ghi như bên dưới (trích datasheet).

MCUCR

      Bit 7 – INT1 gọi là bit cho phép ngắt 1(Interrupt Enable), set bit này bằng 1 nghĩa bạn cho phép ngắt INT1 hoạt động, tương tự, bit INT0 điều khiển ngắt INT0.

      Thanh ghi cờ ngắt chung – GIFR (General Interrupt Flag Register) có 2 bit INTF1 và INTF0 là các bit trạng thái (hay bit cờ - Flag) của 2 ngắt INT1 và INT0. Nếu có 1 sự kiện ngắt phù hợp xảy ra trên chân INT1, bit INTF1 được tự động set bằng 1 (tương tự cho trường hợp của INTF0), chúng ta có thể sử dụng các bit này để nhận ra các ngắt, tuy nhiên điều này là không cần thiết nếu chúng ta cho phép ngắt tự động, vì vậy thanh ghi này thường không được quan tâm khi lập trình ngắt ngoài. Cấu trúc thanh ghi GIFR được trình bày trong hình ngay bên dưới.

MCUCR

      Sau khi đã xác lập các bit sẵn sàng cho các ngắt ngoài, việc sau cùng chúng ta cần làm là set bit I, tức bit cho phép ngắt toàn cục, trong thanh ghi trạng thái chung của chip (thanh ghi SREG, xem lại bài AVR2). Một chú ý khác là vì các chân PD2, PD3 là các chân ngắt nên bạn phải set các chân này là Input (set thanh ghi DDRD). Quá trình thiết lập ngắt ngoài được trình bày trong hình 10.

order
Hình 3. Thiết lập ngắt ngoài.

      Ngắt ngoài với ASM: Dưới đây tôi trình bày cách viết chương trình  sử dụng ngắt ngoài bằng ngôn ngữ ASM, đối với các ngắt khác bạn chỉ cần thêm các DIRECTIVE để định vị các vector ngắt tương ứng và viết chương trình phục vụ ngắt tương ứng.

List 1. Ngắt với ASM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

.CSEG
.INCLUDE "M8DEF.INC"
.ORG 0x000 ; Định vị vị trí đầu tiên
      RJMP BATDAU

.ORG 0x001; Định vị vector ngắt ngoài 0 - INT0 (xem bảng vector)
      RJMP INT0_ISR ; Nhảy đến INT0_ISR nếu có ngắt INT0 xảy ra
.ORG 0x002 ; Định vị vector ngắt ngoài 1 – INT1 (xem bảng vector)
      RJMP INT1_ISR ; Nhảy đến INT1_ISR nếu có ngắt INT1 xảy ra

;Tương tự, định vị các vector ngắt khác ở đây………………..
;………………………………………………………………..

.ORG 0x020 ; Định vị chương trình chính
BATDAU:
; khởi tạo Stack
      LDI R16, HIGH(RAMEND)
      LDI R17, LOW(RAMEND)
      OUT SPH, R16
      OUT SPL, R17

; set chân PD2 và PD3 như các chân input
      LDI R16, 0Bxxxx00xx      ; x là trạng thái do bạn tự chọn, 0 hoặc 1
      OUT DDRD, R16            ; PD2 và PD3 là input
      LDI R16, 0Bxxxx11xx      ; x là trạng thái do bạn tự chọn, 0 hoặc 1
      OUT PORTD, R16          ; mắc điện trở kéo lên cho PD2, PD3


; khởi động  ngắt
      LDI R16, $0B       ; $0B=00001011,  INT1: ngắt cạnh xuống, INT0: ngắt cạnh lên
      OUT MCUCR, R16  ; xuất giá trị điều khiển  ra thanh ghi MCUCR
      LDI R16, $C0      ;$C0=11000000: Enable INT1 và INT0
      OUT GICR, R16 ;xuất giá trị điều khiển  ra thanh ghi  GICR
      SEI ;set bit cho phép ngắt toàn cục
; Chương trình chính
MAIN:
;các công việc mà chương trình chính cần thực hiện………………
;…………………………………………………………………….

      RJMP MAIN

;và đây là định nghĩa trình phục vụ ngắt INT0_ISR…………………
INT0_ISR:
; các công việc cần thực hiện khi có ngắt ……………………
;……………………………………………………………….

      RETI ; phải dùng lệnh RETI để quay về chương trình chính

;và đây là định nghĩa trình phục vụ ngắt INT1_ISR…………………
INT1_ISR:
; các công việc cần thực hiện khi có ngắt ……………………
;……………………………………………………………….
      RETI ; phải dùng lệnh RETI để quay về chương trình chính

      Bạn thấy các các ngắt được định vị nằm giữa vị trí 0x0000, khi mới khởi động, tại ví trí 0x000 là lệnh “RJMP BATDAU”, như thế các lệnh RJMP tại các vector ngắt và các ISR đều không được thực hiện, chúng chỉ được thực hiện một cách tự động khi có ngắt.

       Ngắt ngoài với C: Avr-libc hỗ trợ một thư viện hàm cho ngắt khá hoàn hảo, để sử dụng ngắt trong chương trình viết bằng C (avr-gcc) bạn chỉ cần include file “interrupt.h” nằm trong thư mục con “avr” là xong. file header interrupt.h chứa định nghĩa các hàm và phương thức phục vụ cho viết trình phục vụ ngắt, các vector ngắt không được định nghĩa trong file này mà trong file iom8.h (cho atmega8). Nếu bạn vô tình tìm thấy 1 chương trình ngắt nào đó không include file interrupt.h mà include file signal.h thì bạn đừng ngạc nhiên, đó là cách viết cũ trong avr-gcc, thật ra bạn hoàn toàn có thể sử dụng cách viết cũ vì các phiên bản mới của avr-libc (đi cùng với các bản WinAVR mới) vẫn hỗ trợ cách viết này nhưng không khuyên khích bạn dùng.

       Trong C, các trình phục vụ ngắt có dạng là ISR(vector_name). Trong các phiên bản cũ trình phục vụ ngắt có tên SIGNAL(vector_name), nhưng cũng như file header signal.h, cách viết này vẫn được hỗ trợ trong phiên bản mới nhưng không được khuyến khích.

List 2. Ngắt với C.

1
2
3
4
5
6
#include <avr/interrupt.h>

ISR (vector_name)
{
//user code here
}

      Trong đó vector_name là tên của các vector ngắt định nghĩa sẵn avr-libc, ISR là tên bắt buộc, bạn không được dùng các tên khác tùy ỳ (nhưng có thể dùng SIGNAL như đã trình bày ở trên). Đặc biệt, bạn có thể đặt ISR ở trước hoặc sau chương trình chính đều không ảnh hưởng vì thật ra, đã có khá nhiều “công đoạn” được thực hiện khi bạn gọi ISR (nhưng bạn không thấy và cũng không cần quan tâm). ISR luôn được trình biên dịch đặt ở ngoài vùng vector ngắt như cách chúng ta thực hiện trong ASM, như thế một chương trình sử dụng nhiều loại ngắt sẽ phải có số lượng trình ISR tương ứng nhưng với vector_name khác nhau, mỗi khi có ngắt xảy ra, tùy thuộc vào giá trị của vector_name mà 1 trong các trình ISR được thực thi. Đối với các vector_name, để biết được vector_name cho mỗi loại ngắt, bạn cần tham khảo tài liệu “avr-libc manual”. Bảng 10 tóm tắt các vector_name của một số ngắt thông dụng trên atmega8, bạn chú ý rằng các vector_name trong avr-libc được định nghĩa rất khác nhau cho từng loại chip, bạn nhất thiết  phải sử dụng tài liệu “avr-libc manual” để biết chính xác các vector_name cho loại chip mà bạn đang dùng.

Bảng 3: vector_name cho atmega8.

Vector name Old vector name Description
ADC_vect SIG_ADC ADC Conversion Complete
ANA_COMP_vect SIG_COMPARATOR Analog Comparator
EE_RDY_vect SIG_EEPROM_READY EEPROM Ready
INT0_vect SIG_INTERRUPT0 External Interrupt 0
INT1_vect SIG_INTERRUPT1 External Interrupt Request 1
SPI_STC_vect SIG_SPI Serial Transfer Complete
SPM_RDY_vect SIG_SPM_READY Store Program Memory Ready
TIMER0_OVF_vect SIG_OVERFLOW0 Timer/Counter0 Overflow
TIMER1_CAPT_vect SIG_INPUT_CAPTURE1 Timer/Counter Capture Event
TIMER1_COMPA_vect SIG_OUTPUT_COMPARE1A Timer/Counter1 Compare Match A
TIMER1_COMPB_vect SIG_OUTPUT_COMPARE1B Timer/Counter1 Compare MatchB
TIMER1_OVF_vect SIG_OVERFLOW1 Timer/Counter1 Overflow
TIMER2_COMP_vect SIG_OUTPUT_COMPARE2 Timer/Counter2 Compare Match
TIMER2_OVF_vect SIG_OVERFLOW2 Timer/Counter2 Overflow
TWI_vect SIG_2WIRE_SERIAL 2-wire Serial Interface
USART3_UDRE_vect SIG_USART3_DATA USART3 Data register Empty

III. Ví dụ ngắt ngoài với C.

      Để thực hiện ví dụ sử dụng ngắt ngoài bằng C, tôi sẽ viết lại chương trình ví dụ của bài "cấu trúc AVR" nhưng bằng ngôn ngữ C và sử dụng ngắt. Trong chương trình ví dụ của bài AVR2, chúng ta thực hiện việc đếm lên và đếm xuống dùng 2 button, chúng ta sẽ vẫn thực hiện trên ý tưởng này nhưng có chút thay đổi trong kết nối, trước hết bạn vẽ 1 mạch điện mô phỏng trong Proteus như hình 4.

order
Hình 4. Mạch điện mô phỏng ngắt.

      Kết nối button đếm lên với ngắt INT0, button đếm xuống với INT1, PORTB được chọn làm PORT xuất. Hãy chạy Programmer Notepad,  tạo 1 Project mới tên AVR2-INT, type đoạn code bên dưới vào 1 file new và lưu với tên main.c, add file này vào Project của bạn, sau đó tạo một Makefile cho Project.

List 3. ví dụ ngắt ngoài bằng C.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/delay.h>

volatile int8_t  val=0;    //khai báo 1 biến val 8 bit, có dấu và giá trị khởi tạo bằng 0.
int main(void){

    DDRD=0x00;     //khai báo PORTD là Input để sử dụng 2 chân ngắt.
    PORTD=0xFF;  //sử dụng điện trở nội kéo lên.
    DDRB=0xFF;    //PORTB là Output để xuất LED 7 đoạn
    
    MCUCR|=(1<<ISC11)|(1<<ISC01); //cả 2 ngắt là ngắt cạnh xuống    
    GICR    |=(1<<INT1)|(1<<INT0);    //cho phép 2 ngắt hoạt động
    sei();                                            //set bit I cho phép ngắt toàn cục
    
    DDRC=0xFF;                                   //PORTC là Output
    while (1){                                         //vòng lặp vô tận    
        PORTC++;                                //quét PORTC
        _delay_loop_2(60000);
    }
    return 0;
}

//Trình phục vụ ngắt của  INT0
ISR(INT0_vect){
    val++;                                       //nếu có ngắt INT0 xảy ra, tăng val thêm 1
    if (val>9) val=0;                        //giới hạn không vượt quá 9
    PORTB=val;
}

//Trình phục vụ ngắt của  INT1
ISR(INT1_vect){
    val--;                                        //nếu có ngắt INT1 xảy ra, giảm val đi 1
    if (val<0) val=9;                       //giới hạn không nhỏ hơn 0
    PORTB=val;
}

      Có lẽ đoạn code này khá dễ hiểu nếu các bạn theo dõi từ đầu bài học, tôi chỉ giải thích những nét cơ bản và “mới”. Ý tưởng là chúng ta sử dụng 1 biến tạm 8 bit, có dấu để lưu giá trị đếm, tên biến val,  mỗi khi có ngắt trên chân INT0, tăng val 1 đơn vị và ngược lại khi có ngắt trên INT1, giảm val đi 1, đó là nội dung của 2 trình phục vụ ngắt. Trong chương trình chính, trước hết chúng ta thực hiện việc xác lập hoạt động cho 2 ngắt, sau đó đưa chương trình vào 1 vòng lặp vô tận while(1), PORTC được dùng để kiểm tra rằng chương trình trong vòng lặp vô tận vẫn đang hoạt động. Có lẽ phần khó hiểu nhất trong đoạn code là cách mà tôi dùng để khai báo cho 2 thanh ghi điều khiển ngắt MCUCR và GICR.

      Nếu xem lại bảng tóm tắt các toán tử của C, toán tử “<<” được gọi là toán tử “dịch trái” dùng trên dạng nhị phân của các con số, nếu bạn thấy x=5<<3 nghĩa là dịch các bit nhị phân của 5 sang trái 3 vị trí và gán cho x, như mô tả như sau:

order

     Bạn thấy toàn bộ các bit của 5 đã dịch sang trái 3 vị trí và giá trị của số mới thu được là x=40, chú ý 40=5x8=5x2^3 . Hãy nhìn câu lệnh MCUCR|=(1<<ISC11)|(1<<ISC01), giờ thì bạn đã hiểu (1<<ISC11) nghĩa là dịch số 1 sang trái ISC11 vị trí, và (1<<ISC01) là dịch số 1 sang trái ISC01 vị trí, nhưng ISC11 và ISC01 ở đâu ra và giá trị của chúng là bao nhiêu? Bạn chú ý, khi bạn include file “io.h” thì file “iom8.h” được chèn vào, và trong file này chứa khai báo địa chỉ các thanh ghi của chip atmega8, các tên bit cũng được khai báo sẵn trong file này, nếu bạn mở file iom8.h (thường nằm trong thư mục ~\WinAVR\avr\include\avr) bằng 1 chương trình text editor như notepad, dùng chức năng find bạn sẽ thấy các dòng định nghĩa như sau:

/* MCUCR */
#define SE         7
#define SM2      6
#define SM1      5
#define SM0      4
#define ISC11   3
#define ISC10   2
#define ISC01   1
#define ISC00   0

        Đây là định nghĩa vị trí các bit trong thanh ghi MCUCR, vậy là đã rõ, ISC11=3, ISC01=1, do đó: (1<<ISC11) tương đương (1<<3) = 00001000 (Binary) và (1<<ISC01) = 00000010,  bạn hãy tưởng tượng rằng bạn đã mang số 1 đến các vị trí của ISC11 và ISC01 trong thanh ghi MCUCR. Bây giờ đến lượt toán tử OR bitwise “|”.

(1<<ISC11)                        = 00001000
(1<<ISC01)                        = 00000010
--------------------------------------------------
(1<<ISC11)|(1<<ISC01)     = 00001010

       Gán giá trị này cho MCUCR, đối chiếu với bảng các giá trị của các bit ISC (bảng 9) bạn sẽ thấy chúng ta đang set cho 2 ngắt là falling edge. Điều cuối cùng của câu lệnh set MCUCR là cách rút gọn câu lệnh MCUCR|=(1<<ISC11)|(1<<ISC01) thực chất là MCUCR= MCUCR|  ((1<<ISC11)|(1<<ISC01)), đây là cách set một số bit trong một thanh ghi mà không muốn làm ảnh hưởng đến các bit khác (nhưng bạn phải thật cẩn thận với cách làm này vì có thể sẽ phản tác dụng nếu bạn không nắm rõ), bạn có thể gán trực tiếp MCUCR=(1<<ISC11)|(1<<ISC01), hay nhanh hơn MCUCR=0x0A (0x0A=00001010). Vậy lí do nào khiến tôi biến 1 câu lệnh gán đơn giản thành một “bài toán” khó hiểu, câu trả lời chính là tính tổng quát. Trong các chip AVR khác nhau, vị trí các bit trong các thanh ghi là rất khác nhau, câu lệnh MCUCR=0x0A đúng cho atmega8 nhưng không áp dụng được cho các chip khác trong khi câu lệnh MCUCR=(1<<ISC11)|(1<<ISC01) thì hoạt động tốt, một lí do khác là cách viết gián tiếp này giúp người khác (hay chính bạn sau này) khi đọc code có thể dễ dàng hiểu được ý đồ người viết…
      Tôi nghĩ bạn đã quá hiểu dòng lệnh tiếp theo, GICR |=(1<<INT1)|(1<<INT0). Tôi dừng giải thích đoạn code ở đây và cũng dừng bài AVR3, bạn hãy thực tập bằng cách viết lại đoạn code trên bằng ASM.