Опубликован: 24.11.2024 | Доступ: свободный | Студентов: 2 / 0 | Длительность: 05:47:00
Лекция 6:

Прерываемые приложения

Использование прерываний GPIO в коде

В дальнейшем мы не будем использовать библиотеку Freedom Metal Library для работы с прерываниями. Вместо этого мы будем использовать собственные макрофункции, как это было в предыдущих примерах.

Уровень 1: Работа с КСО

Единственные функции из библиотеки Freedom Metal Library, которые мы будем использовать, это два важных макроса в ./bsp/install/include/metal/csr.h:

#define METAL_CPU_GET_CSR(reg, value) __asm__ volatile("csrr %0, " #reg : "=r"(value)); 
#define METAL_CPU_SET_CSR(reg, value) __asm__ volatile("csrw " #reg ", %0" : : "r"(value));

Как видите, эти макросы создают инструкции csrr и csrw соответственно, поэтому все операции первого уровня будут выполняться с помощью этих макросов.

  • Первый считывает регистр КСО и копирует его содержимое в значение переменной C.
  • Второй копирует значение переменной C в регистр КСО.

Вот как мы можем установить разрешение глобального прерывания (бит 3 в mstatus) и разрешение прерывания MEI (бит 11 в mie):

// Уровень 1: Включить прерывания с MIE в mstatus[3] 
volatile uintptr_t saved_config; 
METAL_CPU_GET_CSR(mstatus,saved_config);
saved_config |= (0x1U<<3); 
METAL_CPU_SET_CSR(mstatus,saved_config);
// Уровень 1: Включить прерывания с MEIE в mie[11] 
METAL_CPU_GET_CSR(mie,saved_config);
saved_config |= (0x1U<<11); 
METAL_CPU_SET_CSR(mie,saved_config);

Что касается обработчиков прерываний, то мы будем работать в прямом режиме. То есть в регистре mtvec будет храниться адрес обработчика прерывания. В следующем примере мы увидим следующий код для этого:

// Уровень 1: Установление базового вектора mtvec 
METAL_CPU_SET_CSR(mtvec,&gpio1_isr);

Уровень 2: ПЛИС

Для работы с регистрами ПЛИС мы будем использовать собственные макросы, поскольку все они отображены в память. Вот макросы, которые мы будем использовать (обратите внимание, что последний из них не является функцией):

#define Red_V_PLIC_GPIO_set_priority(pin,p) *((uint32_t *) (0x0C000020+4*(pin))) = (p) 
#define Red_V_PLIC_clear_ie() *((uint32_t *) 0x0C002000) = 0:/ 
*((uint32_t *) 0x0C002004) = 0 
#define Red_V_PLIC_set_ie1(x) *(
(uint32_t *) 0x0C002000)) |= (1<<(x))
 #define Red_V_PLIC_set_ie2(x) *((uint32_t *) 0x0C002004) |= (1<<(x)) 
#define Red_V_PLIC_claim *((uint32_t *) 0x0C200004)

Вот как мы будем использовать эти макросы:

// Уровень 2: Настройка PLIC для GPIO0
Red_V_PLIC_GPIO_set_priority(1,7); // Контакт 1, приоритет 7
// Уровень 2: PLIC (IE1, бит 9) для GPIO0_1
Red_V_PLIC_clear_ie(); // Отключите все остальные прерывания 
Red_V_PLIC_set_ie1(9); // Включите GPIO0_1

Макрос Red_V_PLIC_claim используется для утверждения прерывания на уровне ПЛИС. Это должно быть сделано внутри обработчика прерывания:

uint32_t plic_id;
Red_V_GPIO_clear_flag(1); // Уровень 3: Очистить флаг GPIO0_1
plic_id = Red_V_PLIC_claim; // Уровень 2: Объявить прерывание GPIO
Red_V_PLIC_claim = plic_id;

Уровень 3: Устройства ввода/вывода

Устройством ввода/вывода, используемым в этом первом приложении, будет модуль GPIO0, и для него мы также будем использовать макросы:

#define Red_V_GPIO_set_ie(x) *((uint32_t *) 0x1001202020) |= (1<<(x)) 
#define Red_V_GPIO_clear_flag(x) *((uint32_t *) 0x10012024) |= (1<<(x))

Эти макросы просто устанавливают и очищают определенные биты в регистрах fall_ie и fall_ip GPIO.

//Уровень 3: Разрешение прерывания по падающему фронту GPIO0_1
Red_V_GPIO_set_ie(1);
Red_V_GPIO_clear_flag(1);

Вспомните, что флаги прерываний обычно не очищаются путем записи в них нулей. Посмотрите на функцию Red_V_GPIO_clear_flag() и обратите внимание, что она вроде бы записывает 1 в интересующий вас бит. Однако именно так эти флаги и очищаются. Запись 0 в эти флаги не имеет никакого эффекта.

Обработчик прерываний GPIO

Теперь давайте посмотрим на код обработчика прерывания GPIO.

Обработчики прерываний могут быть как подпрограммами ассемблера, так и функциями языка Си, но к ним предъявляются два особых требования:

  • Они должны быть выровнены по 64 байтам. Это означает, что младшие 6 бит их адреса должны быть нулевыми.
  • Они должны вернуться с помощью специальной инструкции возврата в машинном режиме mret.

В языке C оба требования выполняются с помощью ключевого слова __attribute__ в прототипе функции следующим образом:

void gpio_isr(void) __attribute__((interrupt, aligned(64))));

Атрибут aligned выполняет первое требование, обеспечивая запуск функции по адресу, выровненному по 64 байтам, а атрибут interrupt выполняет второе, завершая функцию инструкцией mret.

Теперь посмотрите на определение функции. Потратьте время, чтобы понять его смысл.

void gpio_isr(){ 
uint32_t plic_id;
Red_V_set_pin(5); // Включить светодиод
Red_V_GPIO_clear_flag(1); // Уровень 3: Очистить флаг GPIO0_1
plic_id = Red_V_PLIC_claim; // Уровень 2: Заявить прерывание GPIO
Red_V_PLIC_claim = plic_id;
}

Как выглядит обработчик прерывания в ассемблерном коде

Теперь мы рассмотрим, что показывает вид разборки для функции gpio_isr(). Компиляторы реализуют функции в соответствии с соглашением, которое определяет, как передаются параметры, как возвращаются значения и как используются регистры для этих операций. Это соглашение указано в бинарном интерфейсе приложения (ABI), который можно рассматривать как низкоуровневую версию знакомого вам интерфейса прикладного программирования (API).

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

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

Пролог ISR

Вот пролог функции:

          gpio_isr: 
20010e80: addi sp,sp,-32 
20010e82: sw s0,28(sp) 
20010e84: sw a4,24(sp) 
20010e86: sw a5,20(sp) 
20010e88: addi s0,sp,32

Обратите внимание на следующие детали:

  • Функция начинается с адреса 0x20010e80, который действительно выровнен по 64 байтам.
  • Только регистры s0, a4 и a5 сохраняются в стеке, предполагая, что это будут единственные регистры, которые будут изменены в теле функции.
  • s0 получает исходное значение указателя стека для использования его в качестве указателя кадра, как указано в ABI.

Тело ISR

Что касается тела функции, просто обратите внимание, что регистр назначения в каждой инструкции (первый операнд) - это либо a4, либо a5:

 48 Red_V_set_pin(5); // Включите светодиод 
20010e8a: lui a5,0x10012 
20010e8e: addi a5,a5,12 
20010e90: lw a4,0(a5) 
20010e92: lui a5,0x10012 
20010e96: addi a5,a5,12 
20010e98:   ori a4,a4,32 
20010e9c: sw a4,0(a5) 
50 Red_V_GPIO_clear_flag(1); // Уровень 3: Очистить флаг GPIO0_1 
20010e9e: lui a5,0x10012 
20010ea2: addi a5,a5,36 # 0x10012024 
20010ea6: lw a4,0(a5) 
20010ea8:   lui a5,0x10012 
20010eac: addi a5,a5,36 # 0x10012024 
20010eb0: ori a4,a4,2 
20010eb4: sw a4,0(a5) 
51 plic_id = Red_V_PLIC_claim; // Уровень 2: Claim GPIO interrupt 
20010eb6: lui a5,0xc200 
20010eba:   addi a5,a5,4 
20010ebc: lw a5,0(a5) 
20010ebe: sw a5,-20(s0) 
52 Red_V_PLIC_claim = plic_id; 
20010ec2: lui a5,0xc200 
20010ec6: addi a5,a5,4 
20010ec8: lw a4,-20(s0) 
20010ecc: sw a4,0(a5)

Эпилог ISR

Теперь мы рассмотрим эпилог:

 53 } 
20010ece: nop
20010ed0: lw s0,28(sp) 
20010ed2: lw a4,24(sp) 
20010ed4: lw a5,20(sp) 
20010ed6: addi sp,sp,32 
20010ed8: mret

Обратите внимание на следующие детали:

  • Регистры s0, a4 и a5 восстанавливают свои исходные значения перед входом в функцию.
  • Функция завершается специальной инструкцией mret, которая отличается от инструкции ret, используемой для обычных функций.