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
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.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ý.
Nhảy tới ISR và chạy nó
Từ đây là mã bạn viết. Lõi đặt
LRthà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ệ.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.
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
LRmang giá trịEXC_RETURN. Lõi nhận ra và làm ngược lại bước 1.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
| Nhóm | Thanh ghi | Ai lưu | Vì sao |
|---|---|---|---|
| Bị phá hủy được | R0 tới R3, R12, LR, PC, xPSR | Lõi, tự động | ABI 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ên | R4 tới R11 | Trình biên dịch, nếu ISR dùng tới | ABI 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ếp | SP | Không cần lưu | Chính nó là chỗ lưu |
/* Đọ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();
}lan sap gan nhat: PC = 0x08001a3e LR = 0x080012c5 PSR = 0x61000000
/home/user/du-an/src/cam-bien.c:142
#Độ trễ ngắt
| Thành phần | Chu kỳ | Ghi chú |
|---|---|---|
| Hoàn tất lệnh đang chạy | 0 tới 12 | Lệnh chia mất tới 12 chu kỳ và không cắt được |
| Lưu tám thanh ghi | 8 | Một chu kỳ mỗi từ, có thể chồng lấn với việc tra vector |
| Tra bảng vector và nhảy | 4 | Chồng lấn một phần với bước trên |
| Tổng điển hình trên Cortex-M4 | 12 | Con số ST và ARM công bố |
| Cộng thêm nếu đang trong vùng tới hạn | Không giới hạn | Đây là phần bạn kiểm soát được, xem Bài 49.4 |
/* Đ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ễ. */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.
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òng | Ngắt |
|---|---|---|
| Độ trễ | Bằng chu kỳ vòng lặp chính | Khoảng 15 chu kỳ |
| Độ trễ có xác định không | Có, nếu vòng lặp đều | Có, nhưng phụ thuộc ưu tiên |
| Tốn CPU khi không có sự kiện | Có, kiểm liên tục | Không |
| Độ phức tạp mã | Thấp | Cao hơn, cần lo dữ liệu chia sẻ |
| Nguy cơ tranh chấp dữ liệu | Không | Có, xem Bài 49.4 |
| Tiết kiệm điện | Kém, CPU luôn bận | Tốt, CPU ngủ giữa hai sự kiện |
| Xử lý nhiều nguồn cùng lúc | Khó, phải chia thời gian | Tự nhiên, có ưu tiên |
/* 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
| Số hiệu | Tên | Loại | Xảy ra khi |
|---|---|---|---|
| 1 | Reset | Ngoại lệ | Cấp điện hoặc nhấn nút reset |
| 2 | NMI | Ngoại lệ | Ngắt không che được, thường do lỗi clock |
| 3 | HardFault | Ngoại lệ | Lỗi nghiêm trọng, hoặc lỗi khác không được xử lý |
| 4 | MemManage | Ngoại lệ | Vi phạm vùng bộ nhớ được bảo vệ |
| 5 | BusFault | Ngoại lệ | Truy cập địa chỉ không tồn tại |
| 6 | UsageFault | Ngoại lệ | Lệnh không hợp lệ, chia cho không, truy cập lệch |
| 11 | SVCall | Ngoại lệ | Lệnh SVC, dùng cho lời gọi hệ thống |
| 14 | PendSV | Ngoại lệ | Do phần mềm đặt, dùng cho chuyển ngữ cảnh RTOS |
| 15 | SysTick | Ngoại lệ | Bộ đếm SysTick tràn |
| 16 trở lên | IRQ0, IRQ1, ... | Ngắt | Từ ngoại vi, do NVIC quản lý |
/* 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 (;;) { }
}HardFault tai PC = 0x08001a3e
UsageFault: chia cho khong, tai PC = 0x08001a3e -> src/tinh-toan.c:88
BusFault: truy cap dia chi 0x60000000, tai PC = 0x08001b12 -> src/ngoai-vi.c:45
Tự làm thử
- 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.
- Cài
HardFault_HandlerđọcPCtừ khung ngăn xếp, gây lỗi cố ý, rồi dùngaddr2linetìm dòng mã. - Bật
DIV_0_TRPvàUNALIGN_TRP, viết mã chia cho không và xác nhận nó vàoUsageFault_Handler. - Thêm
__WFIvào vòng lặp chính và đo dòng tiêu thụ trước sau bằng đồng hồ đo dòng. - Đ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.