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ắc | Vì sao | |
|---|---|---|
| 1 | Càng ngắn càng tốt, đo bằng micro giây | Mọi chu kỳ trong ISR là chu kỳ ngắt khác phải chờ |
| 2 | Xóa cờ ngắt, và xóa sớm | Không xóa thì ISR được gọi lại vô hạn |
| 3 | Biến chia sẻ với vòng lặp chính phải volatile | Trình biên dịch không biết ISR tồn tại, xem Bài 47.3 |
| 4 | Không printf, không malloc, không free | Chậm, không an toàn khi bị cắt ngang, tốn ngăn xếp |
| 5 | Khô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 đó |
| 6 | Không gọi hàm dài hoặc hàm có thể chặn | Cùng lý do, và bạn thường không biết hàm thư viện làm gì bên trong |
| 7 | Chỉ đặt cờ, xử lý thật ở vòng lặp chính | Hệ 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 vi | Cách xóa cờ | Ví dụ |
|---|---|---|
| Timer | Ghi 0 vào bit cần xóa của SR | TIM2->SR = ~(1u << 0); |
| EXTI | Ghi 1 vào bit cần xóa của PR | EXTI->PR = (1u << 13); |
| DMA | Ghi 1 vào bit tương ứng của LIFCR hoặc HIFCR | DMA2->LIFCR = (1u << 5); |
| UART, cờ RXNE | Đọc thanh ghi DR | uint8_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 DR | uint16_t v = (uint16_t)ADC1->DR; |
| SysTick | Tự xóa khi vào ISR | Khô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ách | Dùng khi | Rủi ro |
|---|---|---|
| Một cờ, không dữ liệu | ISR chỉ báo sự kiện đã xảy ra | Khô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ến | Dữ liệu tới chậm hơn nhiều so với chu kỳ vòng lặp chính | Mất dữ liệu nếu sự kiện tới nhanh |
| Bộ đệm vòng | Dữ liệu tới nhanh hoặc theo cụm | Khô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ệu | Nguyên nhân | Cách kiểm |
|---|---|---|
| Mất dữ liệu UART, cờ ORE tăng | ISR 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ậm | Ngắ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ật | Tổng tải ISR quá cao | Đếm số vòng lặp mỗi giây |
| Ngăn xếp gần tràn | ISR 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ử
- 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.
- 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.
- 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.
- Đo thời gian một ISR bằng chân GPIO và bằng bộ đếm chu kỳ DWT, so hai con số.
- 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
DWTlà cách tiện nhất, và tổng tải ISR nên giữ dưới năm phần trăm.