Обработка прерываний в MIPS-процессорах

 

Теперь - про "устранение причины". Звучит не очень внятно. Поясню, опять же, на примере. Все тот же мультикор, все тот же таймер.

Таймер установлен с некоторыми настройками. Когда он досчитывает до конца - он в своем регистре статуса/управления сбрасывает бит ENABLE и выставляет бит INT. Бит INT транслируется в соответствующий бит регистра QSTR. А единица в регистре QSTR (при единице в соответствующем разряде MASKR) - это исключение прерывания. То есть, если мы по прерыванию таймера выполним свою обработку и сразу сделаем ERET - то единица в QSTR у нас останется, и мы сразу попадем обратно в исключение. И так - до бесконечности. Поэтому в обработчике мы записываем в бит INT ноль, а в бит ENABLE - единицу (конечно, если нам надо перезапустить таймер). Аналогичная картина - со всеми остальными прерываниями. Для внутреннего устройства необходимо сбросить бит в регистре статуса, для программного прерывания - записать ноль, для аппаратного... С аппаратным сложнее. Оно либо в виде короткого импульса (нажатие кнопки) - тогда, конечно, этот абзац можно пропустить. Либо же "источник" внешнего прерывания как-то связан с процессором помимо этой линии - тогда надо как-то сообщить "источнику"о том, что прерывание обрабатывается и можно его "сбросить".

То есть, в общем виде обработчик прерывания будет выглядеть так:

void int_handler() {
unsigned int reg_Cause, reg_Status;
unsigned int ExcCode;
unsigned int IP;
unsigned int IM; // маска прерываний (в регистре Status CP0)
unsigned int ActiveIRQ;
unsigned int ActiveQST;
asm("la $3,reg_Cause"); // адрес переменной reg_Cause
asm("mfc0 $4,$13"); // копируем в $4 содержимое регистра Cause (13й регистр CP0).
asm("sw $4,0($3)"); // сохраняем это значение в переменную
asm("la $3, reg_Status"); // адрес переменной reg_Status
asm("mfc0 $4,$12"); // копируем в $4 содержимое регистра Status
asm("sw $4,0($3)"); // сохраняем в переменную
ExcCode = (reg_Cause >> 2) & 0x1F; // выделяем для удобства поле ExcCode в отдельную переменную
IP = (reg_Cause >> 8) & 0xFF; // аналогично - с полем IP
IM = (reg_Status >> 8) &0xFF; // и с полем IM поступаем также безжалостно
ActiveIRQ = IP & IM;
if ( ExcCode == 0 ) { //  исключение прерывания
................
   if (ActiveIRQ & (1<<4) ) { // внешнее прерывание nIRQ[2]
     // TO DO
   }
   if (ActiveIRQ & (1<<5) ) { // внешнее прерывание nIRQ[3]
     // TO DO
   }
................
   if ( ActiveIRQ & (1<<7) ) { // прерывание от внутреннего устройства
      ActiveQST = MASKR & QSTR;
................
      if ( ActiveQST & (1<<29) ) { // прерывание от таймера
         // TO DO
         ITCSR = 1; // ENABLE = 1, INT = 0 - то есть, сбрасываем прерывание и стартуем таймер заново
      }
................
   }
}

На месте многоточий может быть проверка на остальные прерывания. А может и ничего не быть. Это уже зависит от конкретных условий.

Размещение обработчика

Собственно, размещение - вещь несложная. Секцию .text обработчика надо просто разместить по нужному адресу. Как это сделать - разумеется, зависит от инструментов и IDE. Важный нюанс - на обработчики отводится не так уж много места в данной архитектуре. Например, обработчик исключения TLB Refill находится по адресу 0x8000_0000, а вектор исключения прерывания - по адресу 0x8000_0180. То есть, вроде бы на обработку TLB Refill у нас аж 0x180 байт. Это не мало, но не так уж и много. Иногда может быть необходимо и больше места. Да и отслеживать это бывает сложно. Поэтому проще по вектору обработчика разместить только некий минимальный обработчик, который будет выполнять только сохранение контекста и переход на основную функцию, которая располагается в далеких от вектора областях и поэтому может раскинуться хоть на мегабайт. Разумеется, решать каждому персонально, какой подход избрать. Но так как в компиляторе для мультикоров не предусмотрены ключевые слова, чтобы объявить функцию обработчиком - приходится на векторе располагать такой код, делающий jmp на сишную функцию-обработчик.

Почему нельзя объявить обычную функцию обработчиком? В других компиляторах - можно. Но не в данном конкретном случае. Функция на C в ассемблере будет состоять из сохранения используемых регистров, собственно рабочей части (то, что пишет программист), восстановления использованных регистров и возврата управления. Казалось бы, те же части, из которых состоит обработчик прерываний. За одним маленьким исключением. Обработчик должен заканчиваться инструкцией ERET. А обычная функция заканчивается инструкцией JR $31. В регистре $31 по MIPS-овому соглашению содержится адрес возврата из процедуры. Можно вставить ERET в тело функции. Но тогда она будет выполняться ДО восстановления регистров, что приведет вообще к непредсказуемым последствиям. Поэтому наилучшим пока считаю данный вариант. Разумеется, с использованием других компиляторов ситуация может меняться.

Разрешение прерываний

Казалось бы, мелочь. Однако, стоит ее упомянуть. Сколько каждый эмбеддер в своей жизни потратил времени на "отладку", когда вот не входит в прерывание, и все, хоть ты тресни! А потом оказывалось, что они тупо запрещены. Не стану говорить за всех. Но за себя скажу однозначно и честно - таких моментов было очень много.

Сначала надо разрешить прерывания вообще. За это отвечает бит IE регистра Status сопроцессора CP0 (Status[0]). Далее, необходимо разрешить программные, внешние и внутренние прерывания (разумеется, не все, а те, которые нужны. За это, как мы уже говорили, отвечает поле IM[7:0] регистра Status сопроцессора CP0 (Status[15:8]). Но бит IM[7] отвечает за все прерывания от внутренних устройств разом. Поэтому, чтобы разрешить прерывания от конкретных устройств, необходимо внести соответствующее значение в регистр MASKR. И вот лишь после этого прерывания будут доступны.

Ну, пожалуй, на сегодня и все. Посмотрим, что скажут благодарные читатели... Отзывы важны, жду с нетерпением...