Система прерываний🔗

Общие сведения🔗

Система прерываний СнК Zynq7000 реализована по ARM Legacy Interrupt Scheme (мой термин), а именно: у ядер есть два сигнала – "пина": nIRQ и nFIQ, при активировании уровня (низкого) ядро переходит в состояние обработки соответствующего исключения.

Конкретная СнК содержит специальное устройство – контроллер прерываний, которое:

  • агрегирует запросы на прерывания от всех периферийных устройств;
  • выставляет запрос ядру (ядрам в случае многоядерной процессорной системы);
  • обеспечивает интерфейс информационного обмена для процессора, через который ядро получает информацию о источниках прерываний и управляет ими.

В случае Cortex-A в качестве контроллера прерываний компания ARM предлагает Generic Interrupt Controller (GIC), в СнК Zynq7000 использован GIC v1.0 (PL390).

GIC🔗

Структура🔗

Общее описание системы прерываний SoC Zynq-7000 можно найти в документе UG-585 Zynq 7000 SoC Technical Reference Manual, Chapter 7 Interrupts.

GIC состоит из двух основных частей:

  • Distributor.
  • CPU Interfaces.

Взаимодействие процессора с контроллером прерываний осуществляется с помощью набора регистров, отображённых на память (MMR). Организационно регистры управления GIC отнесены к модулю MPCORE.

Физически GIC в Zynq7000 соединён с процессором через специально выделенную шину, чтобы задержки на общей шине не влияли на динамику работы контроллера прерываний. Сам GIC функционирует на частоте CPU_3x2x, т.е. на тактовой частоте домена L2.

Distributor🔗

Distributor – это аппаратный модуль, который агрегирует запросы на прерывание от всех периферийных устройств СнК и обеспечивает управление свойствами сигналов прерываний. Сюда относится разрешение/запрещение прерываний, управление приоритетом, настройка типов сигналов прерываний (по уровню или по перепаду) и целевых ядер.

Регистры управления дистрибьютором размещаются относительно базового адреса модуля MPCORE со смещением 0x1000.

CPU Interfaces🔗

Модуль процессорных интерфейсов в общем случае является многоканальным (по числу ядер процессора). Его назначение – обеспечить прохождение сигналов прерываний в зависимости от приоритета, предоставить идентификатор прерывания, который необходим для того, чтобы обработчик исключения мог вызвать соответствующую функцию-обработчик прерывания.

Поскольку СнК Zynq7000 содержит двухядерный процессор, то и модуль процессорных интерфейсов тоже состоит из двух одинаковых частей. Каждое ядро процессора "видит" только свою часть модуля интерфейса.

Регистры управления процессорным интерфейсом размещаются относительно базового адреса модуля MPCORE со смещением 0x100.

Источники прерываний🔗

Существует три группы источников прерываний:

  1. Software Generated Interrupts (SGI). Может быть сгенерировано до 16 программных прерываний. Генерирование прерывания производится путём записи определённого значения в специальный регистр в дистрибьюторе.
  2. Private Peripheral Interrupts (PPI). Эта группа прерываний является частной по отношению каждому ядру, т.к. источниками этих прерываний являются периферийные устройства ядра - таймеры и каналы nIRQ/nFIQ.
  3. Shared Peripheral Interrupts (SPI). Самая многочисленная группа, объединяет сигналы от всех периферийный устройство как процессорной части СнК, так и сигналы прерываний от FPGA.

Одно из важных отличий между этими группами заключается в том, как для прерывания указывается целевое ядро.

Для группы PPI ничего указывать не надо - это приватные для ядра источники прерываний (таймеры ядра, прямые каналы nIRQ/nFIQ).

Для SPI целевое ядро указывается в регистрах ICDIPTRn (Interrupt Controller Distributor Interrupt Target Register). Помимо этого в конфигурационных регистрах ICDICFRn (Interrupt Controller Distributor Interrupt Configuration Register) есть флаг для каждого источника прерываний, который определяет, по какой схеме обрабатывать прерывание (1-N – только одно ядро, или N-N – все ядра), но поля, соответствующие SPI, не позволяют сменить этот бит, т.е. жёстко выбрана схема передачи прерывания только одному ядру. В этом регистре можно только менять тип сигнала - по уровню или по перепаду.

Для SGI ситуация ещё более определённая: эти прерывания жёстко установлены на обработку по схеме N-N (все ядра) и тип сигнала прерывания (только по перепаду) не может быть изменён. В общем, это логично и правильно: по перепаду – событие инициирования прерывания одномоментное, выполняемое с помощью команд прикладной программы, т.е. оно (событие) не может удерживать сигнал "по уровню". А возможность передавить сигнал всем ядрам обусловлена необходимостью обеспечить механизм обмена асинхронными сообщениями между ядрами.

Функционирование🔗

Для проверки работоспособности решений использовалась отладочная плата ZedBoard.

Программные прерывания🔗

Программное прерывание инициируется путём записи в регистр ICDSGIR (Interrupt Controller Distributor Software Generated Interrupts Register). При этом указывается целевое ядро (или список ядер) и идентификатор источника программного прерывания. Например1:

INLINE void wrpa(uintptr_t addr, const uint32_t data)
{
    *( reinterpret_cast<volatile uint32_t*>(addr) ) =  data;
}

...

wrpa( GIC_ICDSGIR,                                  // 0b10: send the interrupt on only to the CPU
      (2 << GIC_ICDSGIR_TARGET_LIST_FILTER_BPOS) +  // interface that requested the interrupt     
      PS7IRQ_ID_SW7);                               // rise software interrupt ID7                                                                                                             

В этом примере инициируется программное прерывание с ID 7, целевое ядро - то же самое, которое инициирует прерывание. Аналогичный результат может быть получен:

wrpa( GIC_ICDSGIR,                                  // 0b10: send the interrupt on only to the CPU 
      (0 << GIC_ICDSGIR_TARGET_LIST_FILTER_BPOS) +  // interface that requested the interrupt      
      (0x01 << GIC_ICDSGIR_CPU_TARGET_LIST_BPOS) +  //                                             
      PS7IRQ_ID_SW7);                               // rise software interrupt ID7

Отличие тут в том, что выбор целевых ядер осуществляется по списку (см. описание регистра ICDSGIR). В данном случае указано ядро 0. Если бы, скажем, нужно было указать ядро 1, то значение 0x01 в (0x01 << GIC_ICDSGIR_CPU_TARGET_LIST_BPOS) следовало изменить на 0x02, а если обоим ядрам, то – 0x03. Иными словами, каждому ядру соответствует бит в списке (всего там 8 бит, т.е. GIC поддерживает до 8 ядер).

Прерывания от периферийных устройств🔗

GPIO🔗

Прерывание от периферийных устройств рассмотрено на примере внешнего прерывания от вывода GPIO – из MIO части. Для организации прерывания от внешнего вывода микросхемы нужно настроить соответствующие регистры модуля GPIO и GIC.

GPIO:

    const uint32_t PIN_INT = 50;

    ...

    //-----------------------------------------------
    // set up GPIO interrupt
    gpio_clr_int_sts(PIN_INT);
    gpio_int_pol(PIN_INT, GPIO_INT_POL_HIGH_RISE);
    gpio_int_en(PIN_INT);

Первое, что нужно сделать, это сбросить бит статуса соответствующего вывода MIO. Это необходимо для того, чтобы не возникло ложное прерывание, т.к. любая активность на выводе может взвести флаг прерывания этого вывода в регистре статуса GPIO. И если далее разрешить прерывание, то оно сразу будет осуществлено, хотя событие на выводе происходило в непонятный момент времени, возможно задолго до текущего момента времени. Вряд ли такое прерывание – это то, что хотелось тут получить.

Далее, если требуется, необходимо указать целевое ядро, настроить тип прерывания – по уровню или по перепаду, указать значение уровня/перепада (низкий/отрицательный перепад или высокий/положительный перепад). И разрешить прерывания от GPIO.

GIC:

    gic_set_target(PS7IRQ_ID_GPIO, 1ul << GIC_CPU0);
    gic_set_config(PS7IRQ_ID_GPIO, GIC_EDGE_SINGLE);
    gic_int_enable(PS7IRQ_ID_GPIO);

Тут производится настройка целевого ядра - ядро 0, затем указывается тип прерывания - про фронту (SINGLE означает, что только прерывание предназначено только для одного ядра, а как было указано выше, для SPI доступна только одно ядро, поэтому тут выбора нет). И в заключение - разрешение прерываний от модуля GPIO.

ISR:

void gpio_isr_handler()
{
    ...
    gpio_clr_int_sts(PIN_INT);    // clear GPIO interrupt flag
    ...
}
Triple Timer/Counter🔗

Показан пример TTC в режиме интервального таймера: генерирует прерывания через заданный временной интервал. Исходные данные:

  • источник тактовой частоты – внутренний, 100 МГц;
  • Интервал прерываний – 1 мс.

Настройка:

    // ISR: must be defined
    ps7_register_isr(&ttc0_0_isr, PS7IRQ_ID_TTC0_0);
    ...

    // GIC
    gic_set_priority(PS7IRQ_ID_TTC0_0, 10);
    gic_set_target(PS7IRQ_ID_TTC0_0, GIC_CPU0);
    gic_int_enable(PS7IRQ_ID_TTC0_0);
    ...

    // TTC0_0
    wrpa(TTC_CLK_CTRL1_REG0, 
       (2 << TTC_CLK_CTRL1_PS_V_BPOS) |      // set prescaler to 2^(2+1): 100MHz/8 = 12.5MHz
       (1  << TTC_CLK_CTRL1_PS_EN_BPOS) );   // enable prescaler

    wrpa(TTC_INTERVAL_CNT1_REG0, 12500);                           // 12.5MHz/12500 = 1kHz
    wrpa(TTC_INT_EN1_REG0, 0x1);                                   // enable interval interrupts
    sbpa(TTC_CNT_CTRL1_REG0, 1 << TTC_CNT_CTRL1_INT_BPOS);         // turn on interval mode
    cbpa(TTC_CNT_CTRL1_REG0, 1 << TTC_CNT_CTRL1_DIS_BPOS);         // enable TTC0

ISR:

void ttc0_0_isr()
{
    ...
    rdpa(TTC_INT1_REG0);     // clear TTC0_0 interrupt flag
    ...
}

Общие действия🔗

После всех проделанных выше операций необходимо разрешить работу дистрибьютора и интерфейсов процессора:

    set_bits_pa(GIC_ICDDCR, 0x1); 
    set_bits_pa(GIC_ICCICR, 0x1);
    enable_interrupts();

Код 0x1 соответствует разрешению Secure Interrupts (поскольку процессор при включении питания находится в Secure State). Можно включить и для Non-Secure прерываний (код 0x3), но в данном случае это ни на что не повлияет, т.к. Non-Secure прерываний в текущей конфигурации нет.

Особенность прерываний "по уровню"🔗

Если тип сигнала запроса того или иного прерывания в контроллере прерываний настроен на значение "по уровню" (а для большинства прерываний группы SPI это тип по умолчанию), проявляется следующая особенность.

При тестировании прерывания от внешнего вывода был обнаружен эффект: при однократном нажатии на кнопку (которая подаёт сигнал прерывания на внешний вывод СнК) процессор входит в прерывание дважды. Дребезг кнопки тут ни при чём, т.к. интервал входа в прерывание составляет порядка 300-400 нс.

Исследование этого эффекта показало, что при втором входе значение идентификатора источника прерывания (то, что считывается из регистра ICCIAR) равно 0x3ff, что соответствует специальному коду-значению так называемого spurious interrupt – ложному прерыванию. Ложное прерывание возникает, когда GIC инициировал переход процессора в состояние исключения, но поскольку процесс перехода занимает достаточно большое время (сотни нс), состояние прерывания внутри контроллера прерываний могло измениться - оно было запрещено, отменено, его перехватило другое ядро (хотя это не в данном случае, т.к. в Zynq7000 прерывания группы SPI могут быть адресованы только одному ядру). Т.е. процессор входит в исключение IRQ (потому что его "дёрнули" за пин nIRQ), считывает идентификатор источника прерываний, а готовых к обработке прерываний нет.

В рассматриваем случае происходит следующее: при нажатии на кнопку модуль GPIO инициирует прерывание в GIC, тот в свою очередь переводит процессор в состояние обработки исключения, в обработчике исключений считывается ID источника - в данном случае оно равно 52 (прерывание от GPIO), после чего из таблицы обработчиков прерываний извлекается адрес функции-обработчика прерываний от GPIO, и управление передаётся этой функции. Её код:

void gpio_isr_handler()
{
    gpio_clr_int_sts(PIN_INT);
}

Здесь просто сбрасывается флаг прерывания от внешнего вывода (кнопки), который по сути является запросом на прерывание для GIC.

Далее, процессор возвращается в обработчик исключений, где записывает ID в регистр ICCEOIR, переводя этим состояние прерывания (в контроллере прерываний) в неактивное.

В силу того, что модуль GPIO находится за уровнем L3 т.е. обращение к его регистрам проходит длинный путь (MMU->AXI Bus->Central Interconnect->AXI - APB Bridge) с переходом в более медленные тактовые домены, сброс флага запроса прерывания занимает значительное время. Помимо этого ещё есть некоторая задержка от физического бита-флага запроса прерывания в модуле GPIO до логики контроллера прерываний.

В то же время обработчик прерываний в тестовом примере очень простой - в нём нет никакого полезного прикладного кода, т.е. выход из обработчика прерываний происходит сразу же после команды сброса флага запроса прерываний (процессор не ждёт подтверждения записи, т.к. область памяти MMRs периферийных модулей помечена как Device - запрос на запись буферизуется). Возникает ситуация, при которой, когда процессор уже успевает выйти из обработчика исключений, а флаг запроса прерываний ещё не сброшен или уже сброшен в модуле GPIO, но это значение ещё не успело дойти до GIC - в любом случае контроллер прерываний будучи настроенным на сигнал прерывания "по уровню" реагирует на этот флаг и выставляет снова запрос ядру.

Ядро опять переходит в состояние обработки исключения, но поскольку этот процесс занимает заметный промежуток времени, значение сброшенного флага запроса прерывания от модуля GPIO наконец доходит до контроллера прерываний, и тот снимает запрос на прерывание. Когда ядро читает регистр ICCIAR, оно получается значение 0x3ff, что означает ложное прерывание. Прерывание по сути и является ложным.

В подтверждение этого объяснения был проведён ряд экспериментов. Эффект пропадает, если в после команды сброса флага запроса прерывания внести некоторую задержку, очевидно, достаточную для того, чтобы к моменту выхода процессора из обработчика исключения контроллер прерываний успел "увидеть" сброшенный флаг запроса прерывания от периферийного модуля (GPIO в данном случае).

Очевидно, что такое поведение (ложный вход в обработку прерывания) не является желаемым, поэтому нужно предотвратить его. Для этого можно использовать следующие варианты:

  • Поместить в обработчик прерываний после команды сброса флага код, время выполнения которого гарантировано больше времени прохождения сброса флага запроса прерывания от ядра до периферийного модуля и от последнего до контролера прерываний. Этот способ самый естественный, но задержку трудно контролировать и тем самым обеспечить гарантию.
  • Выполнить после команды сброса флага команду чтения регистра состояния, где размещается этот флаг. При этом возникнет достаточно большая задержка из-за длинного пути прохождения сигналов от периферии до ядра. Но эта задержка замедляет работу программы бесполезным образом.
  • Настроить тип сигнала запроса на прерывания в GIC на значение "по перепаду". Именно этот вариант и использован выше: выражение gic_set_config(PS7IRQ_ID_GPIO, GIC_EDGE_SINGLE);. Этот способ предотвращает ложное прерывание в GIC, т.к. даже если флаг запроса всё ещё установлен в периферийном модуле при выходе процессора из обработчика исключения в основную программу, контроллер прерываний не "видит" в этом запроса, т.к. запрос на прерывание в этом случае для GIC - это изменение значения этого флага с неактивного состояния на активное. А это произойдёт только при следующем нажатии на кнопку. Таким образом, удаётся достичь корректной работы.

Следует отметить, что настройка по умолчанию почти для всех источников прерываний из группы SPI является "по уровню", поэтому этот эффект может проявляться с любым другим периферийным модулем. Нужно внимательно смотреть в каждом случае и применять настройку "по перепаду", если она решает проблему.


  1. wrpa  – "write to phisical address", запись по физическому адресу.