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

Bảy quy tắc viết ISR

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

  • Kể bảy quy tắc viết ISR và lý do từng quy tắc
  • Giải thích vì sao quên xóa cờ làm ngắt lặp vô hạn
  • Cài mẫu ISR đặt cờ và vòng lặp chính xử lý
  • Đo thời gian chạy của một ISR bằng chân GPIO

Một ISR không phải hàm bình thường. Nó chạy giữa lúc chương trình chính đang làm dở việc khác, nó có thể cắt ngang bất kỳ dòng nào, và mọi thứ nó làm đều tính vào độ trễ của các ngắt khác. Bảy quy tắc dưới đây là kết tinh của những điều đó.

#Bảy quy tắc

Quy tắcVì sao
1Càng ngắn càng tốt, đo bằng micro giâyMọi chu kỳ trong ISR là chu kỳ ngắt khác phải chờ
2Xóa cờ ngắt, và xóa sớmKhông xóa thì ISR được gọi lại vô hạn
3Biến chia sẻ với vòng lặp chính phải volatileTrình biên dịch không biết ISR tồn tại, xem Bài 47.3
4Không printf, không malloc, không freeChậm, không an toàn khi bị cắt ngang, tốn ngăn xếp
5Không vòng lặp chờ, không hàm trễChặn mọi ngắt ưu tiên thấp hơn suốt thời gian đó
6Không gọi hàm dài hoặc hàm có thể chặnCùng lý do, và bạn thường không biết hàm thư viện làm gì bên trong
7Chỉ đặt cờ, xử lý thật ở vòng lặp chínhHệ quả tự nhiên của sáu quy tắc trên
isr-mau.c
volatile uint32_t g_tick = 0u;      /* quy tắc 3 */

void TIM2_IRQHandler(void)
{
    if (TIM2->SR & (1u << 0)) {     /* 1. kiểm cờ, vì một ISR có thể
                                          phục vụ nhiều nguồn */
        TIM2->SR &= ~(1u << 0);     /* 2. xóa cờ NGAY, quy tắc 2 */

        ++g_tick;                   /* 3. làm việc tối thiểu, quy tắc 1 */
    }
}

/* Ba dòng. Chạy trong khoảng 12 chu kỳ, tức 0.14 micro giây
   trên chip 84 MHz. Đây là hình dạng mà mọi ISR nên hướng tới. */

#Xóa cờ, quy tắc quan trọng nhất

Quên xóa cờ
void TIM2_IRQHandler(void)
{
    ++g_tick;                      /* quen xoa co */
}

/* Co UIF van bat -> NVIC thay ngat van dang cho ->
   goi lai ISR ngay sau khi no tra ve -> lap VO HAN.

   Trieu chung: chuong trinh chinh khong bao gio chay tiep.
   Thiet bi dung im ngay sau khi bat ngat dau tien. */
Xóa cờ
void TIM2_IRQHandler(void)
{
    TIM2->SR &= ~(1u << 0);        /* xoa co */
    ++g_tick;
}
Ngoại viCách xóa cờVí dụ
TimerGhi 0 vào bit cần xóa của SRTIM2->SR = ~(1u << 0);
EXTIGhi 1 vào bit cần xóa của PREXTI->PR = (1u << 13);
DMAGhi 1 vào bit tương ứng của LIFCR hoặc HIFCRDMA2->LIFCR = (1u << 5);
UART, cờ RXNEĐọc thanh ghi DRuint8_t b = (uint8_t)USART1->DR;
UART, cờ OREĐọc SR rồi đọc DR(void)USART1->SR; (void)USART1->DR;
ADC, cờ EOCĐọc thanh ghi DRuint16_t v = (uint16_t)ADC1->DR;
SysTickTự xóa khi vào ISRKhông cần làm gì

#Mẫu ISR đặt cờ, main xử lý

mau-dat-co.c
#include <stdint.h>

static volatile uint8_t  co_du_lieu = 0u;
static volatile uint8_t  du_lieu    = 0u;

void USART1_IRQHandler(void)
{
    if (USART1->SR & (1u << 5)) {
        du_lieu    = (uint8_t)USART1->DR;   /* đọc DR tự xóa cờ RXNE */
        co_du_lieu = 1u;                    /* đặt cờ, hết việc */
    }
}

int main(void)
{
    khoi_tao();

    for (;;) {
        if (co_du_lieu) {
            co_du_lieu = 0u;

            xu_ly_phuc_tap(du_lieu);        /* nặng bao nhiêu cũng được */
        }

        cap_nhat_den();
        cap_nhat_man_hinh();
    }
}

Ba mức phức tạp của việc truyền dữ liệu

CáchDùng khiRủi ro
Một cờ, không dữ liệuISR chỉ báo sự kiện đã xảy raKhông có, mất cờ chỉ nghĩa là gộp hai sự kiện thành một
Một cờ, một biếnDữ liệu tới chậm hơn nhiều so với chu kỳ vòng lặp chínhMất dữ liệu nếu sự kiện tới nhanh
Bộ đệm vòngDữ liệu tới nhanh hoặc theo cụmKhông mất nếu bộ đệm đủ lớn
mot-co-khong-du-lieu.c
/* Truong hop don gian nhat: ISR chi bao "co gi do da xay ra". */
static volatile uint8_t co_tick_1s = 0u;

void TIM2_IRQHandler(void)
{
    static uint16_t dem = 0u;

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

    if (++dem >= 1000u) {          /* tick 1 ms, dem toi 1 giay */
        dem        = 0u;
        co_tick_1s = 1u;
    }
}

int main(void)
{
    for (;;) {
        if (co_tick_1s) {
            co_tick_1s = 0u;

            in_nhiet_do();         /* moi giay mot lan */
        }
    }
}

#Bốn thứ cấm kỵ trong ISR

#Đo thời gian chạy của ISR

do-bang-gpio.c
/* Cách đơn giản nhất và chính xác nhất: bật chân ở đầu ISR,
   tắt ở cuối, rồi đo bằng bộ phân tích logic hoặc máy hiện sóng. */
void TIM2_IRQHandler(void)
{
    GPIOA->BSRR = (1u << 7);       /* bật chân đo, 2 chu kỳ */

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

    GPIOA->BSRR = (1u << (7 + 16));/* tắt chân đo */
}

/* Ưu điểm:
     - Không ảnh hưởng gì tới thời gian, chỉ 4 chu kỳ tổng
     - Thấy được cả tần suất lẫn thời lượng lẫn hiện tượng lồng nhau
     - Thấy được ISR có bị bỏ lỡ không

   Nhược điểm: cần một chân rảnh và một bộ phân tích logic. */
terminal
# do bang bo phan tich logic, 84 MHz
kenh 0 (TIM2 ISR):
  so lan trong 1 giay : 1000
  do rong nho nhat    : 0.19 us
  do rong lon nhat    : 0.24 us
  tong thoi gian      : 0.21 ms  = 0.021 phan tram CPU

kenh 1 (USART1 ISR):
  so lan trong 1 giay : 11518
  do rong lon nhat    : 0.42 us
  tong thoi gian      : 4.1 ms   = 0.41 phan tram CPU
do-bang-bo-dem-chu-ky.c
/* Cách thứ hai: dùng bộ đếm chu kỳ DWT có sẵn trong Cortex-M3 trở lên. */
#include "stm32f4.h"

static void dwt_khoi_tao(void)
{
    CoreDebug->DEMCR |= (1u << 24);      /* TRCENA */
    DWT->CYCCNT       = 0u;
    DWT->CTRL        |= 1u;              /* CYCCNTENA */
}

static volatile uint32_t isr_lau_nhat = 0u;

void TIM2_IRQHandler(void)
{
    uint32_t t0 = DWT->CYCCNT;

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

    uint32_t d = DWT->CYCCNT - t0;

    if (d > isr_lau_nhat) isr_lau_nhat = d;
}

/* Ưu điểm: không cần phần cứng ngoài, và ghi lại được giá trị xấu nhất.
   Nhược điểm: hai lần đọc DWT tốn khoảng 4 chu kỳ, và DWT không có
   trên Cortex-M0. */
terminal
> isr-stat
TIM2   : lau nhat  18 chu ky = 0.21 us
USART1 : lau nhat  35 chu ky = 0.42 us
EXTI13 : lau nhat  12 chu ky = 0.14 us

tong tai CPU uoc tinh: 0.43 phan tram

Ba dấu hiệu ISR quá dài

Dấu hiệuNguyên nhânCách kiểm
Mất dữ liệu UART, cờ ORE tăngISR khác chặn ISR của UART quá lâuĐo độ rộng từng ISR bằng chân GPIO
Tick hệ thống chạy chậmNgắt SysTick bị bỏ lỡSo đồng hồ nội với đồng hồ thật sau một giờ
Vòng lặp chính chạy giậtTổng tải ISR quá caoĐếm số vòng lặp mỗi giây
Ngăn xếp gần trànISR lồng nhau nhiều tầng, hoặc ISR dùng nhiều biến cục bộĐo mức dùng ngăn xếp, xem Bài 47.1

Tự làm thử

  1. Viết ISR quên xóa cờ và xác nhận chương trình chính không bao giờ chạy tiếp.
  2. Viết ISR xóa cờ ở cuối, tạo sự kiện tần số cao, và đếm số sự kiện bị mất.
  3. Cài mẫu một cờ một biến, gửi dữ liệu nhanh, rồi so số byte nhận được với bản dùng bộ đệm vòng.
  4. Đo thời gian một ISR bằng chân GPIO và bằng bộ đếm chu kỳ DWT, so hai con số.
  5. Cài bộ đếm vòng lặp nhàn rỗi và tính tải CPU khi bật lần lượt từng ngắt.

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ảy quy tắc gói lại thành một câu: ISR phải ngắn, phải xóa cờ, và phải để phần nặng cho vòng lặp chính.
  • Xóa cờ ngay đầu ISR chứ không phải cuối, để sự kiện xảy ra trong lúc xử lý không bị xóa mất.
  • Mỗi ngoại vi có cách xóa cờ riêng: ghi 0, ghi 1, đọc thanh ghi dữ liệu, hoặc tự xóa. Phải tra tài liệu.
  • Cấm printf, cấm cấp phát động, cấm vòng lặp chờ, và cấm gọi hàm thư viện không biết bên trong làm gì.
  • Đo bằng chân GPIO là cách chính xác nhất, đo bằng DWT là cách tiện nhất, và tổng tải ISR nên giữ dưới năm phần trăm.