Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 49.126 phút đọc

Cơ chế ngắt

Sau bài này bạn sẽ làm được

  • Kể sáu bước từ lúc ngắt xảy ra tới lúc quay về chương trình chính
  • Hiểu khung ngăn xếp mà lõi tự lưu và phần phải tự lưu
  • Ước lượng độ trễ ngắt và các yếu tố làm nó tăng
  • Phân biệt hỏi vòng với ngắt và chọn đúng

Ngắt là cơ chế cho phép phần cứng cắt ngang chương trình đang chạy, làm một việc gấp, rồi trả lại quyền điều khiển đúng chỗ vừa dừng. Nó là thứ duy nhất cho phép một vi điều khiển phản ứng kịp thời với thế giới bên ngoài.

#Sáu bước phần cứng làm

Bước 1 và 5 do lõi tự làm bằng phần cứng. Bước 3 và 4 là mã bạn viết.
  1. Lõi tự lưu tám thanh ghi lên ngăn xếp

    Gồm xPSR, PC, LR, R12, R3, R2, R1, R0. Đây là những thanh ghi mà quy ước gọi hàm cho phép hàm được gọi phá hủy, nên lưu chúng là đủ để ISR viết bằng C thuần chạy được.

  2. Tra bảng vector để lấy địa chỉ ISR

    Bảng vector nằm ở đầu Flash. Số hiệu ngắt nhân bốn cộng địa chỉ bảng là ra ô chứa địa chỉ hàm xử lý.

  3. Nhảy tới ISR và chạy nó

    Từ đây là mã bạn viết. Lõi đặt LR thành một giá trị đặc biệt gọi là EXC_RETURN để nhận ra đây là lời gọi từ ngoại lệ.

  4. ISR xóa cờ ngắt

    Bắt buộc, và Bài 49.3 nói kỹ. Quên bước này thì ngắt được kích hoạt lại ngay lập tức và ISR chạy vô hạn.

  5. Lõi khôi phục tám thanh ghi

    Xảy ra khi ISR thực hiện lệnh trả về với LR mang giá trị EXC_RETURN. Lõi nhận ra và làm ngược lại bước 1.

  6. Chạy tiếp đúng chỗ vừa dừng

    Vì PC đã được khôi phục từ ngăn xếp. Chương trình chính hoàn toàn không biết gì đã xảy ra.

#Khung ngăn xếp

Tám thanh ghi trên do lõi tự lưu. Tám thanh ghi dưới chỉ cần lưu khi chuyển ngữ cảnh giữa các tác vụ.
NhómThanh ghiAi lưuVì sao
Bị phá hủy đượcR0 tới R3, R12, LR, PC, xPSRLõi, tự độngABI cho phép hàm được gọi phá hủy chúng, nên hàm C có thể dùng ngay
Phải giữ nguyênR4 tới R11Trình biên dịch, nếu ISR dùng tớiABI bắt hàm được gọi phải khôi phục chúng trước khi trả về
Con trỏ ngăn xếpSPKhông cần lưuChính nó là chỗ lưu
doc-khung.c
/* Đọc PC của lệnh bị ngắt, hữu ích khi gỡ lỗi HardFault. */
void HardFault_Handler(void)
{
    uint32_t *khung;

    /* Bit 2 của EXC_RETURN cho biết dùng MSP hay PSP.
       Ở đây đọc LR bằng hợp ngữ vì C không chạm được vào nó. */
    __asm volatile (
        "tst  lr, #4        \n"
        "ite  eq            \n"
        "mrseq %0, msp      \n"
        "mrsne %0, psp      \n"
        : "=r" (khung)
    );

    uint32_t r0   = khung[0];
    uint32_t r1   = khung[1];
    uint32_t r2   = khung[2];
    uint32_t r3   = khung[3];
    uint32_t r12  = khung[4];
    uint32_t lr   = khung[5];
    uint32_t pc   = khung[6];      /* địa chỉ lệnh gây lỗi */
    uint32_t psr  = khung[7];

    (void)r0; (void)r1; (void)r2; (void)r3;
    (void)r12; (void)lr; (void)psr;

    luu_vao_flash(pc);             /* ghi lại rồi khởi động lại */
    NVIC_SystemReset();
}
terminal
> fault-log
lan sap gan nhat:
  PC  = 0x08001a3e
  LR  = 0x080012c5
  PSR = 0x61000000
# Tra địa chỉ đó về dòng mã nguồn
arm-none-eabi-addr2line -e firmware.elf 0x08001a3e
/home/user/du-an/src/cam-bien.c:142

#Độ trễ ngắt

Độ trễ ngắt
Khoảng thời gian từ lúc phần cứng phát tín hiệu ngắt tới lúc lệnh đầu tiên của ISR được thực hiện.
Thành phầnChu kỳGhi chú
Hoàn tất lệnh đang chạy0 tới 12Lệnh chia mất tới 12 chu kỳ và không cắt được
Lưu tám thanh ghi8Một chu kỳ mỗi từ, có thể chồng lấn với việc tra vector
Tra bảng vector và nhảy4Chồng lấn một phần với bước trên
Tổng điển hình trên Cortex-M412Con số ST và ARM công bố
Cộng thêm nếu đang trong vùng tới hạnKhông giới hạnĐây là phần bạn kiểm soát được, xem Bài 49.4
do-do-tre.c
/* Đo độ trễ thật: timer kích ngắt, ISR bật chân GPIO ngay lệnh đầu.
   Đo bằng bộ phân tích logic khoảng cách giữa hai cạnh. */
void TIM2_IRQHandler(void)
{
    GPIOA->BSRR = (1u << 5);      /* lệnh đầu tiên, không làm gì khác */

    TIM2->SR &= ~(1u << 0);

    GPIOA->BSRR = (1u << (5 + 16));
}

/* Timer cấu hình để cạnh ra ở chân khác đồng thời với sự kiện ngắt,
   nên khoảng cách giữa hai cạnh chính là độ trễ. */
terminal
# do bang bo phan tich logic, chip 84 MHz
do tre nho nhat  : 0.155 us = 13 chu ky
do tre trung binh: 0.179 us = 15 chu ky
do tre lon nhat  : 0.393 us = 33 chu ky

Lan lon nhat trung luc chuong trinh chinh dang chay lenh chia.
# Cùng phép đo nhưng vòng lặp chính có tắt ngắt 5 us
# do lai
do tre lon nhat  : 5.21 us = 437 chu ky

Vung toi han chi phoi hoan toan.

#Hỏi vòng hay ngắt

Tiêu chíHỏi vòngNgắt
Độ trễBằng chu kỳ vòng lặp chínhKhoảng 15 chu kỳ
Độ trễ có xác định khôngCó, nếu vòng lặp đềuCó, nhưng phụ thuộc ưu tiên
Tốn CPU khi không có sự kiệnCó, kiểm liên tụcKhông
Độ phức tạp mãThấpCao hơn, cần lo dữ liệu chia sẻ
Nguy cơ tranh chấp dữ liệuKhôngCó, xem Bài 49.4
Tiết kiệm điệnKém, CPU luôn bậnTốt, CPU ngủ giữa hai sự kiện
Xử lý nhiều nguồn cùng lúcKhó, phải chia thời gianTự nhiên, có ưu tiên
hai-cach.c
/* Cách hỏi vòng: đơn giản, dễ lập luận, tốn CPU */
int main(void)
{
    khoi_tao();

    for (;;) {
        if (USART1->SR & (1u << 5)) {
            uint8_t b = (uint8_t)USART1->DR;

            xu_ly(b);
        }

        if (nut_da_nhan()) doi_che_do();

        cap_nhat_den();
    }
}

/* Cách ngắt: phức tạp hơn, nhưng CPU ngủ được */
int main(void)
{
    khoi_tao();
    bat_ngat();

    for (;;) {
        if (co_du_lieu) { co_du_lieu = 0u; xu_ly(du_lieu); }
        if (co_nut)     { co_nut     = 0u; doi_che_do(); }

        cap_nhat_den();

        __WFI();      /* ngủ cho tới khi có ngắt bất kỳ */
    }
}

#Ngoại lệ khác ngắt

Ngoại lệ
Khái niệm rộng hơn ngắt. Mọi sự kiện làm lõi rời khỏi luồng thực thi bình thường đều là ngoại lệ, kể cả những sự kiện do chính lệnh đang chạy gây ra.
Số hiệuTênLoạiXảy ra khi
1ResetNgoại lệCấp điện hoặc nhấn nút reset
2NMINgoại lệNgắt không che được, thường do lỗi clock
3HardFaultNgoại lệLỗi nghiêm trọng, hoặc lỗi khác không được xử lý
4MemManageNgoại lệVi phạm vùng bộ nhớ được bảo vệ
5BusFaultNgoại lệTruy cập địa chỉ không tồn tại
6UsageFaultNgoại lệLệnh không hợp lệ, chia cho không, truy cập lệch
11SVCallNgoại lệLệnh SVC, dùng cho lời gọi hệ thống
14PendSVNgoại lệDo phần mềm đặt, dùng cho chuyển ngữ cảnh RTOS
15SysTickNgoại lệBộ đếm SysTick tràn
16 trở lênIRQ0, IRQ1, ...NgắtTừ ngoại vi, do NVIC quản lý
fault-chi-tiet.c
/* Bat ba loai loi rieng thay vi de tat ca do ve HardFault. */
void fault_bat_chi_tiet(void)
{
    SCB->SHCSR |= (1u << 16)      /* MemManage enable */
                | (1u << 17)      /* BusFault enable */
                | (1u << 18);     /* UsageFault enable */

    /* Bat them canh bao khi chia cho khong va khi truy cap lech */
    SCB->CCR |= (1u << 4)         /* DIV_0_TRP */
              | (1u << 3);        /* UNALIGN_TRP */
}

void UsageFault_Handler(void)
{
    uint32_t ufsr = (SCB->CFSR >> 16) & 0xFFFFu;

    if (ufsr & (1u << 9)) bao("chia cho khong");
    if (ufsr & (1u << 8)) bao("truy cap lech");
    if (ufsr & (1u << 0)) bao("lenh khong hop le");

    for (;;) { }
}

void BusFault_Handler(void)
{
    uint32_t bfsr = (SCB->CFSR >> 8) & 0xFFu;

    if (bfsr & (1u << 7))         /* BFARVALID */
        bao_dia_chi(SCB->BFAR);   /* dia chi gay loi */

    for (;;) { }
}
terminal
# Không bật chi tiết: mọi lỗi đều thành HardFault
> gay-loi --chia-khong
HardFault tai PC = 0x08001a3e
# Sau khi bật
> gay-loi --chia-khong
UsageFault: chia cho khong, tai PC = 0x08001a3e
-> src/tinh-toan.c:88
> gay-loi --doc-dia-chi-la
BusFault: truy cap dia chi 0x60000000, tai PC = 0x08001b12
-> src/ngoai-vi.c:45

Tự làm thử

  1. Viết ISR bật một chân GPIO ở lệnh đầu và đo độ trễ ngắt bằng bộ phân tích logic hoặc máy hiện sóng.
  2. Cài HardFault_Handler đọc PC từ khung ngăn xếp, gây lỗi cố ý, rồi dùng addr2line tìm dòng mã.
  3. Bật DIV_0_TRP và UNALIGN_TRP, viết mã chia cho không và xác nhận nó vào UsageFault_Handler.
  4. Thêm __WFI vào vòng lặp chính và đo dòng tiêu thụ trước sau bằng đồng hồ đo dòng.
  5. Đo chi phí ngắt với sự kiện thưa và với sự kiện liên tiếp, rồi tính phần tiết kiệm được nhờ đuôi ghép.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Lõi Cortex-M tự lưu tám thanh ghi khi vào ngoại lệ, và bộ tám đó khớp chính xác với quy ước gọi hàm nên ISR viết được bằng C thuần.
  • Ô thứ bảy của khung ngăn xếp là PC, tức địa chỉ lệnh bị ngắt. Đây là chìa khóa để gỡ lỗi HardFault.
  • Độ trễ ngắt điển hình khoảng 15 chu kỳ, nhưng vùng tới hạn dài có thể đẩy nó lên hàng trăm lần.
  • Hỏi vòng đơn giản và dễ lập luận, ngắt tiết kiệm CPU và điện. Chọn theo tần suất sự kiện và số việc phải làm cùng lúc.
  • Bật ba loại lỗi chi tiết và bật bẫy chia cho không là hai thay đổi miễn phí biến lỗi âm thầm thành lỗi rõ ràng.