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

Bảng vector và NVIC

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

  • Viết bảng vector ngắt và đặt nó vào đúng phân đoạn
  • Hiểu cơ chế hàm yếu và vì sao viết sai tên là ISR không chạy
  • Đặt độ ưu tiên và biết số nhỏ là ưu tiên cao
  • Giải thích ngắt lồng nhau và nhóm ưu tiên

Bảng vector là cầu nối giữa số hiệu ngắt và hàm bạn viết. Cầu nối đó hoạt động bằng cách khớp tên hàm, và một chữ cái sai làm ISR không bao giờ chạy mà trình biên dịch không hề cảnh báo.

#Bảng vector

Bảng vector
Một mảng con trỏ hàm đặt ở đầu Flash. Phần tử thứ n chứa địa chỉ hàm xử lý ngoại lệ số n. Lõi tra bảng này mỗi lần có ngoại lệ.
startup.c
#include <stdint.h>

extern uint32_t _estack;      /* do tệp linker định nghĩa */

void Reset_Handler(void);
void Default_Handler(void);

/* Ngoại lệ của lõi Cortex-M */
void NMI_Handler(void)        __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void)  __attribute__((weak, alias("Default_Handler")));
void MemManage_Handler(void)  __attribute__((weak, alias("Default_Handler")));
void BusFault_Handler(void)   __attribute__((weak, alias("Default_Handler")));
void UsageFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SVC_Handler(void)        __attribute__((weak, alias("Default_Handler")));
void DebugMon_Handler(void)   __attribute__((weak, alias("Default_Handler")));
void PendSV_Handler(void)     __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void)    __attribute__((weak, alias("Default_Handler")));

/* Ngắt từ ngoại vi, tên do nhà sản xuất quy định */
void WWDG_IRQHandler(void)    __attribute__((weak, alias("Default_Handler")));
void EXTI0_IRQHandler(void)   __attribute__((weak, alias("Default_Handler")));
void TIM2_IRQHandler(void)    __attribute__((weak, alias("Default_Handler")));
void USART1_IRQHandler(void)  __attribute__((weak, alias("Default_Handler")));
/* ... còn hàng chục hàm nữa ... */

__attribute__((section(".isr_vector"), used))
void (*const bang_vector[])(void) = {
    (void (*)(void))&_estack,      /*  0: đỉnh ngăn xếp ban đầu */
    Reset_Handler,                 /*  1: reset */
    NMI_Handler,                   /*  2 */
    HardFault_Handler,             /*  3 */
    MemManage_Handler,             /*  4 */
    BusFault_Handler,              /*  5 */
    UsageFault_Handler,            /*  6 */
    0, 0, 0, 0,                    /*  7 tới 10: dành riêng */
    SVC_Handler,                   /* 11 */
    DebugMon_Handler,              /* 12 */
    0,                             /* 13: dành riêng */
    PendSV_Handler,                /* 14 */
    SysTick_Handler,               /* 15 */

    /* Từ đây là ngắt ngoại vi, IRQ0 trở đi */
    WWDG_IRQHandler,               /* 16, IRQ0 */
    /* ... */
    TIM2_IRQHandler,               /* 44, IRQ28 */
    /* ... */
    USART1_IRQHandler,             /* 53, IRQ37 */
};

void Default_Handler(void)
{
    for (;;) { }      /* xem mục cuối bài về cách tốt hơn */
}

Tệp linker đặt bảng vector đúng chỗ

linker.ld
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 96K
}

_estack = ORIGIN(RAM) + LENGTH(RAM);      /* đỉnh ngăn xếp */

SECTIONS
{
    .isr_vector :
    {
        KEEP(*(.isr_vector))      /* KEEP: đừng xóa dù không ai gọi */
    } > FLASH                     /* đặt ở ĐẦU Flash */

    .text :
    {
        *(.text*)
        *(.rodata*)
    } > FLASH

    /* ... phần còn lại, xem Bài 50.4 ... */
}

#Hàm yếu và cái bẫy tên sai

Ký hiệu yếu
Một định nghĩa hàm được đánh dấu weak sẽ bị thay thế nếu ở nơi khác có định nghĩa mạnh cùng tên. Nếu không có, định nghĩa yếu được dùng.
co-che.c
/* Trong startup.c: dinh nghia YEU, tro toi Default_Handler */
void TIM2_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));

/* Trong main.c: dinh nghia MANH cua ban */
void TIM2_IRQHandler(void)
{
    TIM2->SR &= ~(1u << 0);
    ++g_tick;
}

/* Trinh lien ket thay ban co dinh nghia manh nen dung no,
   va bang vector tro toi ham cua ban. Khong can khai bao gi them. */

Default_Handler nên nói cho bạn biết chuyện gì

default-handler.c
#include <stdint.h>

/* Doc so hieu ngoai le dang chay tu thanh ghi IPSR. */
void Default_Handler(void)
{
    uint32_t ipsr;

    __asm volatile ("mrs %0, ipsr" : "=r" (ipsr));

    uint32_t so_ngoai_le = ipsr & 0x1FFu;
    int32_t  so_irq      = (int32_t)so_ngoai_le - 16;

    uart_gui_chuoi("NGAT KHONG CO ISR: exception ");
    uart_in_uint(so_ngoai_le);

    if (so_irq >= 0) {
        uart_gui_chuoi(", IRQ ");
        uart_in_int(so_irq);
    }

    uart_gui_chuoi("\r\n");

    for (;;) { }
}
terminal
picocom -b 115200 /dev/ttyUSB0
NGAT KHONG CO ISR: exception 44, IRQ 28
# Tra bảng vector: IRQ28 là TIM2
grep -n 'IRQ28\|TIM2_IRQ' startup.c
58:    TIM2_IRQHandler,               /* 44, IRQ28 */

#NVIC: bật, tắt, ưu tiên

NVIC
Nested Vectored Interrupt Controller. Khối phần cứng trong lõi Cortex-M quản lý mọi ngắt ngoại vi: bật, tắt, đặt độ ưu tiên, và quyết định ngắt nào được chạy khi nhiều ngắt cùng chờ.
nvic.c
#include "stm32f4.h"

void ngat_khoi_tao(void)
{
    /* 1. Bật ngắt Ở NGOẠI VI. Không có bước này thì ngoại vi
          không bao giờ gửi tín hiệu tới NVIC. */
    USART1->CR1 |= (1u << 5);      /* RXNEIE */
    TIM2->DIER  |= (1u << 0);      /* UIE */

    /* 2. Đặt độ ưu tiên. SỐ NHỎ là ưu tiên CAO. */
    NVIC_SetPriority(USART1_IRQn, 1u);
    NVIC_SetPriority(TIM2_IRQn,   3u);

    /* 3. Bật ngắt ở NVIC */
    NVIC_EnableIRQ(USART1_IRQn);
    NVIC_EnableIRQ(TIM2_IRQn);

    /* 4. Bật ngắt toàn cục, thường đã bật sẵn sau reset */
    __enable_irq();
}
Hàm CMSISViệc làm
NVIC_EnableIRQ(irq)Cho phép ngắt này tới lõi
NVIC_DisableIRQ(irq)Chặn ngắt này, sự kiện vẫn được ghi nhớ
NVIC_SetPriority(irq, p)Đặt độ ưu tiên, số nhỏ là cao
NVIC_GetPriority(irq)Đọc lại độ ưu tiên
NVIC_SetPendingIRQ(irq)Kích hoạt ngắt bằng phần mềm
NVIC_ClearPendingIRQ(irq)Xóa yêu cầu ngắt đang chờ
NVIC_GetPendingIRQ(irq)Hỏi ngắt này có đang chờ không
NVIC_SystemReset()Khởi động lại chip

#Ngắt lồng nhau

Ví dụ lồng nhau
/* TIM2 uu tien 3, USART1 uu tien 1 (cao hon).

   Thoi gian ->

   main         |----|                              |--------
   TIM2 ISR           |------|          |--|
   USART1 ISR                 |--------|

   1. main dang chay, ngat TIM2 toi -> vao TIM2_IRQHandler
   2. Dang trong TIM2 ISR, ngat USART1 toi
      USART1 uu tien CAO hon -> ngat luon TIM2 ISR
   3. USART1 ISR chay xong -> quay ve TIM2 ISR dung cho vua dung
   4. TIM2 ISR chay xong -> quay ve main

   Neu USART1 uu tien THAP hon hoac BANG TIM2 thi no phai cho
   TIM2 ISR chay xong. */

Nhóm ưu tiên

nhom-uu-tien.c
/* STM32F4 cai dat 4 bit uu tien, tuc 16 muc.
   Bon bit do chia thanh hai phan:

     uu tien CHIEM QUYEN (preempt): quyet dinh ai ngat duoc ai
     uu tien PHU (sub):             quyet dinh ai chay truoc khi CUNG cho

   Diem chia do truong PRIGROUP trong SCB->AIRCR quyet dinh,
   va NVIC_SetPriorityGrouping ghi thang gia tri do vao. */

NVIC_SetPriorityGrouping(3u);      /* 4 bit chiem quyen, 0 bit phu */
NVIC_SetPriorityGrouping(5u);      /* 2 bit chiem quyen, 2 bit phu */
NVIC_SetPriorityGrouping(7u);      /* 0 bit chiem quyen, 4 bit phu */

/* Voi PRIGROUP 7: KHONG ngat nao ngat duoc ngat nao.
   Day la cach don gian nhat, va du cho hau het du an. */
Đối số CMSISBit chiếm quyềnBit phụSố mức lồng nhau
0 tới 34016
4318
5224
6132
7041, tức không lồng nhau

#Chọn mức ưu tiên

MứcDùng choVí dụYêu cầu
0, cao nhấtSự kiện tuyệt đối không được lỡDừng khẩn cấp, phát hiện mất điện, bảo vệ quá dòngISR dưới 20 chu kỳ
1Giao tiếp tốc độ caoUART ở 921600 baud, SPI, encoderISR dưới 50 chu kỳ
2Tick hệ thống và điều khiểnSysTick, vòng điều khiển PIDISR dưới vài trăm chu kỳ
3, thấp nhất thường dùngSự kiện chậm, không gấpNút bấm, cảm biến nhiệt độ, DMA báo xongKhông có yêu cầu chặt

Kích hoạt ngắt bằng phần mềm

ngat-phan-mem.c
/* Mau rat huu ich: ISR uu tien cao lam phan gap,
   roi kich hoat mot ngat uu tien THAP de lam phan cham. */

void USART1_IRQHandler(void)      /* uu tien 1, phai nhanh */
{
    uint8_t b = (uint8_t)USART1->DR;

    ring_them(b);

    if (b == '\n')
        NVIC_SetPendingIRQ(EXTI0_IRQn);   /* nho ISR khac xu ly */
}

void EXTI0_IRQHandler(void)       /* uu tien 3, duoc phep cham */
{
    EXTI->PR = (1u << 0);         /* xoa co */

    phan_tich_dong_lenh();        /* viec nang, hang tram chu ky */
}

/* Ky thuat nay goi la nua duoi, muon tu nhan Linux.
   Uu diem so voi dat co cho vong lap chinh: no chay
   NGAY khi khong con ngat nao uu tien cao hon,
   thay vi cho vong lap chinh quay lai. */

Tự làm thử

  1. Viết tệp startup với bảng vector đầy đủ cho chip của bạn, kèm hàm yếu cho mọi ngắt.
  2. Cố tình viết sai tên một ISR, chạy chương trình, rồi dùng nm tìm ra chữ W chỉ ra lỗi.
  3. Cài Default_Handler in ra số hiệu ngoại lệ, rồi gây một ngắt không có ISR và đọc thông báo.
  4. Đặt hai ngắt ở hai mức ưu tiên khác nhau, bật một chân GPIO trong mỗi ISR, và quan sát lồng nhau bằng bộ phân tích logic.
  5. Đo mức dùng ngăn xếp cao nhất với nhóm ưu tiên 4 và với nhóm 0, so hai con số.

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

  • Bảng vector là mảng con trỏ hàm ở đầu Flash, và ô đầu tiên chứa đỉnh ngăn xếp chứ không phải địa chỉ hàm.
  • Cơ chế hàm yếu cho phép ghi đè ISR chỉ bằng cách định nghĩa hàm cùng tên. Sai một chữ là ISR không bao giờ chạy mà không có cảnh báo nào.
  • Bốn công tắc mắc nối tiếp: clock ngoại vi, bit cho phép ngắt của ngoại vi, NVIC_EnableIRQ, và PRIMASK.
  • Số ưu tiên nhỏ là ưu tiên cao. Ngắt cùng mức không lồng nhau, và nhóm ưu tiên 4 tắt hẳn lồng nhau.
  • Cho Default_Handler in ra số hiệu ngoại lệ là cải tiến rẻ nhất và hữu ích nhất trong toàn bộ phần khởi động.