В этой лекции мы поговорим об эффективной обработке ввода-вывода. Сначала мы поговорим о двух методах обработки ввода-вывода, называемых опросом и прерываниями. Затем мы создадим простое, но ужасное GPIO-приложение, просто чтобы показать вам, насколько плохим может быть опрос. Для того чтобы решить эту проблему, нам придется узнать, как обычно работают прерывания, затем как они работают в микроконтроллере FE310. А после этого, мы создадим простое приложение, управляемое прерываниями GPIO, чтобы познакомить вас с процессом обработки прерываний. И после того, как мы создадим это приложение, мы сможем перейти к исправлению нашего примера опроса с помощью прерываний. Вы увидите, что эта замена приложения намного лучше, чем первая попытка. Нам еще многому предстоит научиться.
Так что давайте приступим к делу.
К концу этой лекции вы должны:
Существует несколько подходов к работе с устройствами ввода/вывода. Мы использовали прямой подход, называемый опросом, поскольку он заключается в опросе всех используемых устройств ввода-вывода в поисках того, которое требует нашего внимания (внимания процессора). Именно этим занимались наши приложения в бесконечном главном цикле.
Более эффективная техника обработки ввода-вывода называется обработкой прерываний. Она использует уникальный аппаратный элемент в ядре процессора, который выполняет опрос за вас (грубо говоря). Всякий раз, когда устройство ввода-вывода требует внимания процессора, логика этого устройства устанавливает бит, который запускает процесс прерывания
Когда происходит прерывание, центральный процессор останавливает выполнение кода, который выполнялся в данный момент, сохраняет часть состояния процессора в стеке и переходит к выполнению функции обратного вызова, известной как подпрограмма обслуживания прерывания (Interrupt Service Routine, ISR), или обработчик прерывания.
Каждое поддерживаемое прерывание имеет свой обработчик прерывания. Задача программиста - написать эти обработчики прерываний с кодом, который будет либо принимать входящие данные, либо отправлять исходящие данные. После выполнения обработчика прерывания, вместо того чтобы вернуться в виде вызванной функции, процессор возобновляет работу, которую он выполнял, когда был прерван. Обработка прерываний - это высокоэффективная техника обработки ввода-вывода, в то время как опрос влечет за собой ненужные накладные расходы, которые обычно впустую расходуют наше драгоценное процессорное время. Именно поэтому опрос обычно делает пользовательские интерфейсы менее отзывчивыми, а вычисления медленными.
Давайте рассмотрим мысленный эксперимент для лучшего понимания опроса и обработки прерываний. Предположим, вы отправились в торговый центр, потому что вам нужно купить несколько вещей в разных магазинах. Прежде чем отправиться в магазины, вы заходите в фуд-корт и заказываете еду, которая займет некоторое время. Вы не знаете, сколько времени потребуется на приготовление. Это может быть от 10 до 45 минут. Как же вы собираетесь делать покупки?
Подход, основанный на опросе
Подход, основанный на опросе, можно сравнить с походом в магазин за покупками на 5 минут, а затем возвращением в фуд-корт и вопросом о вашем блюде. Если оно еще не готово, тогда вы идете за покупками на 5 минут, возвращаетесь в фуд-корт и спрашиваете снова. Так продолжается до тех пор, пока ваше блюдо не будет готово. Когда оно готово, вы садитесь и едите. Затем вы можете продолжить покупки.
Подход, основанный на прерываниях
Подход, основанный на прерываниях, похож на то, как если бы вы оставили номер своего мобильного телефона, чтобы персонал ресторана позвонил вам, когда ваше блюдо будет готово. Когда вам позвонят, вы должны будете прекратить делать то, что делали в этот момент, и пойти в фуд-корт, чтобы поесть. После еды вы можете вернуться в магазин и продолжить начатое. Таким образом, вы можете ходить за покупками без всякого беспокойства.
Какой подход лучше?
Теперь мы добавим несколько чисел, чтобы нам было легче провести сравнение. Это будет очень упрощенно: допустим, что путь от магазина до фуд-корта занимает 2 минуты (таким образом, путь туда и обратно займет 4 минуты), и что в итоге еда будет готова через 30 минут после заказа.
При подходе, основанном на опросе, ваше драгоценное время будет использовано следующим образом: через 34 минуты вы потратите на покупки 20 минут, а на вопросы о еде - 14 минут.
(рис 5.1) Использование времени при голосовании за еду в торговом центре
Эффективность
Эффективность - одна из немногих метрик, которые волнуют инженеров. Она означает, насколько эффективно используется ограниченный ресурс. Стремление к максимизации эффективности эквивалентно стремлению к минимизации отходов. Существует столько же форм измерения эффективности, сколько и ограниченных ресурсов. Эффективность часто обозначается греческой буквой eta ($$\eta$$).
Если мы определим эффективность как процент от общего времени, которое вы потратили на полезную работу (покупки), то в данном случае эффективность будет следующей:
$$\eta_{Tine}=\frac{t_{Shopping}}{t_{Total}}*100%=\frac{20minutes}{34minutes}*100%\approx59%$$Эффективность также можно рассчитать, как количество посещений фуд-корта, которые оказались полезными, из общего числа посещений:
$$\eta_{Visits}=\frac{Visits_{Useful}}{Visits_{Total}}*100%=\frac{1visit}{4visits}*100%\approx25%$$При подходе, основанном на прерывании, вы потратите 30 минут на покупки, плюс время, необходимое для того, чтобы ответить на звонок и забрать продукты:
(рис 5.2) Использование времени при ожидании телефонного звонка по поводу еды в торговом центре
Рассчитывая те же показатели эффективности для подхода с прерыванием, мы получаем следующее:
| Метод опроса | Метод прерываний | |
|---|---|---|
| Эффективность использования времени | 59% | 94% |
| Эффективность визитов на фудкорт | 25% | 100% |
Ускорение
Еще одна метрика - Ускорение, которая измеряет, насколько быстрее работает один вариант по сравнению с другим. К тому времени, когда вы получите еду, вы проведете за покупками 20 минут в случае с методом опроса, и 30 минут в случае использования прерываний. Таким образом, если сравнивать варианты по истечении 32 минут, то ускорение при использовании прерываний по сравнению с методом опроса составит:
$$Speedup=\frac{Time_{Polling}}{Time_{Interrupts}}=\frac{30minutes}{20minutes}=1.5$$Другими словами, с помощью прерываний работа выполняется в 1,5 раза быстрее по сравнению с методом опросом в данном примере.
Теперь рассмотрим встроенную систему. Здесь мы имеем код опроса для автомобильного приложения, которое управляет приборной панелью в автомобиле. Предположим, что все функции являются блокирующими, и каждой из них требуется время, указанное в комментарии справа, чтобы вернуться.
void main(){
while(1){
read_touch_screen(); // 100ms
read_GPS(); // 300ms
read_fuel_level(); // 200ms
read_velocity(); // 200ms
read_temperature(); // 100ms
refresh_door_light(); // 100ms
refresh_display(); // 200ms
}
}
Очевидно, что это приложение демонстрирует чрезмерное использование кода в главном цикле. Обратите внимание, что первая функция считывает данные с сенсорного экрана, а последняя обновляет дисплей. Вся работа между ними, безусловно, сделает приложение неотзывчивым. Также обратите внимание, что нет необходимости проверять каждое устройство ввода-вывода на каждой отдельной итерации главного цикла. Вот как выглядит выполнение с течением времени:
(рис 5.3) Хронология исполнения кода приложения приборной панели при использовании подхода опроса
Теперь давайте рассмотрим альтернативу, управляемую прерываниями.
void touch_screen_ISR(); // 100ms
void GPS_ISR(); // 300ms
void fuel_level_ISR(); // 200ms
void velocity_ISR(); // 200ms
void temperature_ISR(); // 100ms
void door_light_ISR(); // 100ms
void main(){
while(1)
refresh_display(); // 200ms
}
Обратите внимание, что в главном цикле вызывается только функция обновления дисплея. Остальные функции заменены обработчиками прерываний, также известными как подпрограммы обслуживания прерываний (ISR). Эти ISR занимают столько же времени, сколько и их аналоги, но они выполняются только тогда, когда возникает необходимость (открывается дверь, пользователь касается экрана, меняется уровень топлива и так далее). Это ответы на запросы ввода-вывода, а не команды на чтение или вывод чего-либо, как в подходе с опросами.
Необходимым условием эффективной работы прерываний является то, что запросы на прерывание должны поступать с гораздо меньшей частотой, чем запросы основного потока. Другими словами, прерывания должны возникать не постоянно, а время от времени, иначе они будут мешать выполнению основной задачи. Все, что постоянно требует внимания, должно находиться в основном цикле.
Здесь мы имеем возможную временную шкалу примера с прерыванием:
(рис 5.4) Хронология исполнения кода приложения приборной панели при использовании прерываний
Поскольку запросы ввода-вывода происходят нечасто, основной цикл может работать без сбоев.
Возможно, вы не до конца уверены, что прерывания всегда лучше, чем опрос, поэтому давайте посмотрим, насколько плохим может быть приложение с опросом, если оно плохо спроектировано. Опросом можно злоупотреблять до такой степени, что несколько задач будут конкурировать за процессорное время, что приведет к плохому выполнению общей задачи.
Простым примером для иллюстрации этого является объединение двух приложений, которые мы уже видели ранее: Проект Blinky и Демонстрация управления яркостью светодиодов. Исходный код этих приложений будет объединен в один файл C. Это объединенное приложение будет использовать те же входные переключатели и светодиоды, что и демонстрация ШИМ. Вот принципиальная схема:
(рис 5.5) Принципиальная схема демонстрационного приложения, использующего метод опроса
Кнопка 0 уменьшит яркость внешнего светодиода на ступень, а кнопка 1 увеличит ее на ступень.
На этот раз внешний светодиод будет иметь 5 уровней яркости, увеличивающихся с шагом в 25%, поэтому он также будет переходить от 0% к 100%. Тем временем, встроенный светодиод будет мигать с частотой 1 Гц независимо.
Это контакты ввода/вывода, которые мы будем использовать для подключения кнопок и внешнего светодиода:
(рис 5.6) Виды сверху и снизу Red-V Thing Plus, показывающие контакты, которые будут использоваться в демонстрационном приложении для опроса (Изображение взято с сайта SparkFun Electronics, по лицензии CC BY 2.0)
Здесь представлены две фотографии фактических аппаратных соединений:
(рис 5.7) Аппаратные соединения для приложения управления светодиодом, основанном на методе опроса
Наша первая попытка использует простую адаптацию кода на языке C для обоих приложений, используя подход опроса.
Здесь приведен код приложения для опроса светодиодов. Если вы следите за развитием событий, создайте новый проект на основе проекта примера hello, как обычно, и скопируйте следующий код, чтобы заменить содержимое файла hello.c.
/******************************************************************************
Red-V Thing Plus LED_Polling demo,
by Eduardo Corpe?o
Using the built-in LED and two external buttons to control its brightness,
while the on-board LED blinks at 1Hz independently.
Date: June 20, 2022
Developed using Freedom Studio v4.18.0.2021-04-1 on Windows 10
LICENSE: This code is released under the MIT License
(http://opensource.org/licenses/MIT)
******************************************************************************/
#include <metal/gpio.h>
#include <metal/pwm.h>
#include <stdint.h>
#include <time.h>
#define Red_V_enable_output(x) * ((uint32_t * ) 0x10012008) |= (1 << (x))
#define Red_V_set_pin(x) * ((uint32_t * ) 0x1001200C) |= (1 << (x))
#define Red_V_clear_pin(x) * ((uint32_t * ) 0x1001200C) = ~(1 << (x))
#define Red_V_read_pin(x)( * ((uint32_t * ) 0x10012000) (1 << (x))))
#define Red_V_enable_pullup(x) * ((uint32_t * ) 0x10012010) |= (1 << (x))
#define Red_V_enable_DS(x) * ((uint32_t * ) 0x10012014) |= (1 << (x))
#define Red_V_enable_XOR(x) * ((uint32_t * ) 0x10012040) |= (1 << (x))
// Пользовательская функция задержки
void delay(int number_of_microseconds) {
clock_t start_time = clock(); // Хранение времени запуска
while (clock() < start_time + number_of_microseconds);
}
int main(void) {
unsigned int dc = 50; // По умолчанию рабочий цикл равен 50%
struct metal_gpio * gpio_0; // Экземпляр GPIO
struct metal_pwm * pwm_1; // Экземпляр PWM
gpio_0 = metal_gpio_get_device(0);
// Конфигурация встроенного светодиода
metal_gpio_disable_input(gpio_0, 5);
Red_V_enable_output(5);
metal_gpio_disable_pinmux(gpio_0, 5);
metal_gpio_set_pin(gpio_0, 5, 1);
// Конфигурация кнопок
metal_gpio_enable_input(gpio_0, 0); // включить ввод 0
metal_gpio_enable_input(gpio_0, 1); // включить ввод 1
metal_gpio_disable_output(gpio_0, 0); // отключить вывод 0
metal_gpio_disable_output(gpio_0, 1); // отключить вывод 1
metal_gpio_disable_pinmux(gpio_0, 0); // отключить альтернативные функции для пина 0
metal_gpio_disable_pinmux(gpio_0, 1); // // отключить альтернативные функции для пина 1
Red_V_enable_pullup(0); // включить подтягивающий резистор для пина 0
Red_V_enable_pullup(1); // включить подтягивающий резистор для пина 1
// Конфигурация ШИМ. Мы будем использовать PWM1_1, который использует GPIO0_19.
Red_V_enable_DS(19); // Сила высокого выхода
Red_V_enable_XOR(19); // Инвертируем выход для активно-высокого ШИМа
pwm_1 = metal_pwm_get_device(1); // Получение экземпляра ШИМ-устройства
while (pwm_1 == NULL); // Остановка.Что-то пошло не так
metal_pwm_enable(pwm_1); // Включить ШИМ1
metal_gpio_disable_pinmux(gpio_0, 20); // Отключить альтернативные функции для пина 20
metal_gpio_disable_pinmux(gpio_0, 21); // Отключить альтернативные функции для пина 21
metal_gpio_disable_pinmux(gpio_0, 22); // Отключить альтернативные функции для пина 22
// Установите частоту ШИМ1 на 1 кГц
metal_pwm_set_freq(pwm_1, 1, 1000); // Установите частоту ШИМ1_1 на 1 кГц для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE); // Устанавливаем рабочий цикл для ШИМ1_1
metal_pwm_trigger(pwm_1, 1, METAL_PWM_CONTINUOUS);
// Запуск в непрерывном режиме
while (1) {
// Кнопки и код ШИМ внешнего светодиода
if (Red_V_read_pin(0) == 0) { // Чтение входного пина 0
dc = (dc > 0) ? dc - 25 : dc; // Шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc,
METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программное ожидание
while (Red_V_read_pin(0) == 0); // Ждем, пока кнопка не будет отпущена
delay(50000); // Программное ожидание
} else if (Red_V_read_pin(1) == 0) { // Считывание входного пина 1
dc = (dc < 100) ? dc + 25 : dc; // Шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программное ожидание
while (Red_V_read_pin(1) == 0); // Ждем, пока кнопка будет отпущена
delay(50000); // Программное ожидание
}
// Код мигающего светодиода
Red_V_clear_pin(5); // Выключение встроенного светодиода
delay(500000); // 500 мс в микросекундах
Red_V_set_pin(5); // Включение встроенного светодиода
delay(500000); // 500 мс в микросекундах
}
return 0; // Недостижимый код
}
Обратите внимание на следующие детали в коде:
delay(), ту, которая вызывает функцию clock() из time.h. Мы не используем быструю и грязную функцию задержки, использованную ранее для предотвращения дребезга кнопок.Попробуйте приложение и проверьте, правильно ли оно работает. Спойлер: это не так!
Чтобы ответить на этот вопрос, давайте обратимся к части кода, посвященной опросу, в главном цикле:
while (1) {
// Кнопки и код ШИМ внешнего светодиода
if (Red_V_read_pin(0) == 0) { // Чтение входного пина 0
dc = (dc > 0) ? dc - 25 : dc; // Шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc,
METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программное ожидание
while (Red_V_read_pin(0) == 0); // Ждем, пока кнопка не будет отпущена
delay(50000); // Программное ожидание
} else if (Red_V_read_pin(1) == 0) { // Считывание входного пина 1
dc = (dc < 100) ? dc + 25 : dc; // Шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программное ожидание
while (Red_V_read_pin(1) == 0); // Ждем, пока кнопка будет отпущена
delay(50000); // Программное ожидание
}
// Код мигающего светодиода
Red_V_clear_pin(5); // Выключение встроенного светодиода
delay(500000); // 500 мс в микросекундах
Red_V_set_pin(5); // Включение встроенного светодиода
delay(500000); // 500 мс в микросекундах
}
Обратите внимание, что везде присутствуют задержки, отнимающие время. Проблема в том, что функция задержки - это так называемая блокирующая функция, которая блокирует выполнение до тех пор, пока время не истечет.
Еще один блокирующий элемент можно увидеть в циклах while между задержками в строках кода для предотвращения дребезга. Проблема заключается в том, что приложение не будет выполняться до тех пор, пока кнопка не будет отпущена. Это не является проблемой для части ШИМ, потому что генератор ШИМ - это отдельная часть аппаратного обеспечения, которая работает независимо. Тем не менее, код мигающего светодиода нуждается в процессоре.
Альтернативой блокирующим функциям являются неблокирующие функции. Когда вызывается неблокирующая функция, она вызывается кратко перед возвратом. Если же она должна дождаться выполнения внешней задачи, она возвращается, откладывая обработку результата этой задачи.
Неблокирующая функция задержки может запустить таймер и вернуться. Затем остальная часть кода может использовать опрос для проверки выполнения таймера, не задерживая выполнение в блокирующем цикле.
При разумном использовании в опросе нет ничего плохого. Однако опросом можно злоупотреблять несколькими способами, например, блокировать функции, добавлять слишком много кода в основной цикл или без необходимости прерывать выполнение основной задачи. Именно тогда он становится проблемой.
Прерывания подходят для обработки нечастых запросов ввода-вывода, таких как нажатие клавиш или спорадические входящие сообщения от другого устройства. В целом, обработчики прерываний должны быть намного быстрее, чем скорость поступления прерываний.
Если это условие выполняется, то прерывания помогают сделать встроенные приложения лучше во многих отношениях. Давайте рассмотрим некоторые из этих преимуществ:
Прерывания всегда присутствуют в профессиональных приложениях, но они не очень популярны в сообществе Arduino, возможно, из-за кажущейся сложности процесса по сравнению с простым написанием кода обработки ввода-вывода в основном цикле, что, как мы знаем, является плохой практикой. Если вы решили никогда не использовать прерывания в своих приложениях, то создание программ-игрушек - это максимум, на что вы способны. Если вы серьезно занимаетесь встраиваемыми системами, прерывания, рано или поздно, должны быть добавлены в ваш инструментарий.
Все микроконтроллеры поддерживают прерывания, и схема может варьироваться от очень простой государственной машины в CPU до очень сложного внешнего блока.
Важно понимать, что обработка прерываний - это аппаратный, а не программный процесс. Как программист, вы не должны беспокоиться об этом процессе, но должны предоставить всю информацию, необходимую аппаратуре для правильной обработки прерываний. По этой причине вы должны знать основные элементы, используемые в контроллере прерываний.
Контроллеры прерываний в микроконтроллерах реализуют некоторые варианты следующих аппаратных средств:
(рис 5.8) Типичное оборудование контроллера прерываний
Обратите внимание на следующие детали схемы:
Когда сигнал прерывания достигает центрального процессора, запускается процесс подготовки к обработке прерывания:
Мотивация использования подхода, основанного на прерываниях, заключается в том, чтобы избежать остановки выполнения кода с помощью блокирующих функций или блокирующих циклов, поэтому следует, что обработчики прерываний должны быть краткими.
Основное приложение было прервано (подумайте об этом, как о том, чтобы заставить кого-то выйти с совещания), поэтому ваш код должен перейти прямо к делу. В обработчике прерывания есть место только для срочного кода. Все остальное должно быть отложено до главного цикла. Поэтому общее эмпирическое правило - избегать длинных вычислений в обработчиках прерываний (ISR).
Кроме того, нет ничего хорошего в том, чтобы пройти через все трудности разработки приложения, управляемого прерываниями только для того, чтобы написать блокирующие подпрограммы обслуживания прерываний. Даже случайно. Поэтому, помимо отказа от использования блокирующего кода, еще одним общим правилом при написании обработчиков прерываний является отказ от использования циклов. Всегда пишите код обработчика прерываний без циклов, если это возможно.
Для получения дополнительных советов по написанию обработчиков прерываний, вы можете прочитать статью в блоге Embedded Artistry.
Before diving into the interrupt usage process for the FE3190, here is an attempt to not scare you away by looking at the big picture.
Прежде чем погрузиться в процесс использования прерываний для FE3190, здесь мы попытаемся не отпугнуть вас, взглянув на общую картину.
Оказывается, FE310 использует несколько уровней одного и того же процесса, который мы только что видели, потому что у него есть несколько уровней контроллеров прерываний. Это происходит потому, что RISC-V описывает использование прерываний только в своей спецификации режимов привилегий. За пределами этой спецификации производители должны реализовать свои контроллеры прерываний и свои модули ввода/вывода, которые имеют некоторые шаги конфигурации для прерываний.
Микроконтроллер FE310 использует 3 уровня сигналов разрешения/отсрочки прерывания, и весь процесс может быть запутанным из-за большого количества регистров и лишних шагов. Чтобы упростить ситуацию, на следующей схеме приведены регистры, участвующие в интересующем нас процессе:
(рис 5.9) Регистры обработки прерываний для устройств ввода/вывода в микроконтроллере FE310
Ядро процессора обслуживает только 3 возможных источника прерываний: MEI, MSI и MTI. Нас интересует MEI, машинное внешнее прерывание. Вот регистры процессора, показанные на схеме (они не привязаны к памяти):
Этот контроллер отвечает за так называемые глобальные прерывания, которые поступают от устройств ввода/вывода (GPIO, PWM, SPI, I2C и т.д.). Вот интересующие нас регистры с привязкой к памяти:
enable1, а старшая половина - enable2.0x0C000004 - 0x0C0000D0.Различные устройства ввода/вывода могут по-разному реализовывать свои регистры прерываний, но обычно они имеют следующие регистры, привязанные к памяти:
pwmcfg.Микроконтроллер FE310 поддерживает несколько типов прерываний. Другими словами, прерывания могут поступать из нескольких различных источников. На следующей диаграмме показана архитектура прерываний FE310. Обратите внимание, что есть два контроллера прерываний:
(рис 5.10) Архитектура прерываний FE310 (Изображение из руководства пользователя FE310-G002, воспроизведено с разрешения компании SiFive, Inc.)
Platform-Level Interrupt Controller (ПЛИС) - это глобальный контроллер прерываний в системе RISC-V. Этот контроллер обеспечивает так называемый сигнал Machine External Interrupt ядру E31. Как показано на схеме, этот контроллер отвечает за внешние устройства ввода/вывода.
Core-Local Interruptor (CLINT) генерирует локальные прерывания, связанные с определенными аппаратными потоками (или сокращенно harts). Прерывания, обеспечиваемые этим контроллером, - это прерывание машинного программного обеспечения и прерывание машинного таймера.
Далее мы рассмотрим более подробное описание регистров, участвующих в настройке и использовании прерываний.
Эти три типа прерываний называются машинными, поскольку они работают в режиме привилегий RISC-V, называемом машинным режимом, который имеет полный доступ к памяти, вводу/выводу и низкоуровневым функциям. Машинный режим - это самый привилегированный режим процессора RISC-V. Обычные потоки, такие как главная функция в ваших программах, работают в режиме пользователя, который предназначен для потоков с ограниченными привилегиями.
Машинный режим RISC-V полагается на 8 регистров управления и состояния (CSR) для обработки исключений и прерываний. Это первый уровень регистров прерываний, с которыми мы должны работать.
Здесь приведено описание КСО (нам не обязательно использовать все из них):
Как получить доступ к КСО
КСО не отображаются в памяти, поэтому в базовом целочисленном ISA есть 6 специальных инструкций для доступа к этим регистрам. Это более 12% набора команд, поэтому следует, что эти команды должны быть важными.
Единственным способом доступа к этим регистрам являются инструкции CSRRW{I}, CSRRS{I} и CSRRC{I}. Не волнуйтесь, инструментарий компилятора позволяет встраивать инструкции ассемблера в код на языке Си.
Нас интересует использование Контроллера прерывания на уровне платформы (ПЛИС) для прерываний от внешних устройств. Это второй уровень регистров прерываний, с которыми мы должны работать.
Как мы видели на принципиальной схеме ПЛИС имеет 52 источника. Это все контакты, подключенные к модулям ввода/вывода, включая их функции GPIO и аппаратного ввода/вывода. Нумерацию этих источников прерываний мы можем найти в таблице 26 руководства FE310:
(рис 5.11) Сопоставление источников прерываний ПЛИС в микроконтроллере FE310 (Изображение из руководства пользователя FE310-G002, воспроизведено с разрешения компании SiFive, Inc)
Для обработки запросов ввода-вывода в модуле ПЛИС мы будем использовать следующие регистры, привязанные к памяти:
Как и большинство микроконтроллеров, FE310 оснащен множеством устройств ввода/вывода. Кроме принципа работы эти устройства отличаются способом реализации прерываний:
Вспомните регистры GPIO из таблицы 52 руководства пользователя:
(рис 5.12) Смещение и описание регистров конфигурации GPIO (Изображение из руководства пользователя FE310-G002, воспроизведено с разрешения компании SiFive, Inc.)
Обратите внимание, что в адресах с 0x18 по 0x34 находятся регистры разрешения прерывания и ожидания прерывания. Как уже упоминалось, они предназначены для генерации прерываний в случаях падающего фронта, нарастающего фронта, высокого состояния и низкого состояния. Как и другие регистры модуля GPIO, эти регистры разрешают прерывания или указывают на ожидающие прерывания, относящиеся ко всем 32 выводам модуля GPIO, побитово.
Прежде чем исправлять наше неисправное приложение "Мигание и яркость", давайте начнем с более простого приложения, чтобы ознакомить вас с процессом.
Вспомните, как работает наш первый демонстрационный вход GPIO: Кнопка 0 выключает светодиод на плате, а кнопка 1 включает его, вот так:
while(1){
if(Red_V_read_pin(0) == 0) // Считать входной пин 0
Red_V_clear_pin(5); // Выключить светодиод
else if(Red_V_read_pin(1) == 0) // Считать входной пин 1
Red_V_set_pin(5); // Включить светодиод
}
Затем измените это приложение, чтобы заменить обработку кнопки 1 на управление прерыванием.
Для этого приложения можно использовать то же оборудование, которое мы используем для приложения "Мигание и яркость".
В дальнейшем мы не будем использовать библиотеку 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 соответственно, поэтому все операции первого уровня будут выполняться с помощью этих макросов.
Вот как мы можем установить разрешение глобального прерывания (бит 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.
Обработчики прерываний могут быть как подпрограммами ассемблера, так и функциями языка Си, но к ним предъявляются два особых требования:
В языке 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
Обратите внимание на следующие детали:
Тело 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
Обратите внимание на следующие детали:
Теперь, когда мы увидели многие части кода, пришло время собрать его воедино. Вот код для простого приложения GPIO, управляемого прерываниями. Если вы следите за развитием событий, создайте новый проект на основе проекта примера hello, как обычно, и скопируйте следующий код, чтобы заменить содержимое файла hello.c.
Запустите приложение, чтобы убедиться, что оно работает правильно.
/******************************************************************************
Red-V Thing Plus Interrupt_Demo,
автор Эдуардо Корпеньо
Для управления используется встроенный светодиод и две внешние кнопки.
Одна кнопка управляется опросом, а другая - прерываниями.
Дата: 20 июня 2022 г.
Разработано с использованием Freedom Studio v4.18.0.2021-04-1 на Windows 10
ЛИЦЕНЗИЯ: Этот код выпущен под лицензией MIT License
(http://opensource.org/licenses/MIT)
******************************************************************************/
#include <metal/gpio.h> //include GPIO library, https://sifive.github.io/freedom-metal-docs/apiref/gpio.html
#include <stdint.h>
#include <metal/csr.h>
// Macro Functions
#define Red_V_enable_output(x) *((uint32_t *) 0x10012008) |= (1< <(x))
#define Red_V_set_pin(x) *((uint32_t *) 0x1001200C) |= (1<< (x))
#define Red_V_clear_pin(x). *((uint32_t *) 0x1001200C) = ~(1< <(x))
#define Red_V_read_pin(x) (*((uint32_t *) 0x10012000) (1< <(x))
#define Red_V_enable_pullup(x) *((uint32_t *) 0x10012010) |= (1< <(x))
#define Red_V_GPIO_set_ie(x) *((uint32_t *) 0x10012020) |= (1< <(x))
#define Red_V_GPIO_clear_flag(x) *((uint32_t *) 0x10012024) |= (1< <(x))
#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)
void gpio_isr(void) __attribute__((interrupt, aligned(64)));
void gpio_isr(){
uint32_t plic_id;
Red_V_set_pin(5); //Включить светодиод
Red_V_GPIO_clear_flag(1); // Level 3: Очистить флаг GPIO0_1
plic_id = Red_V_PLIC_claim; // Level 2: Объявить прерывание GPIO
Red_V_PLIC_claim = plic_id;
}
int main (void){
struct metal_gpio *gpio_0; // Создать экземпляр GPIO
gpio_0 = metal_gpio_get_device(0);
// Пины устанавливаются при инициализации, поэтому мы должны отключить его, когда используем его как вход/выход
metal_gpio_disable_input(gpio_0, 5);
// Устанавливаем gpio как выход
//metal_gpio_enable_output(gpio_0, 5);
Red_V_enable_output(5);
// Пины имеют более одной функции, убедитесь, что мы отключаем все подключенное
metal_gpio_disable_pinmux(gpio_0, 5);
metal_gpio_enable_input(gpio_0, 0); // включить вход 0
metal_gpio_enable_input(gpio_0, 1); // включить вход 1
metal_gpio_disable_output(gpio_0, 0); // отключить вывод 0
metal_gpio_disable_output(gpio_0, 1); // отключить вывод 1
metal_gpio_disable_pinmux(gpio_0, 0); // отключение альтернативных функций для вывода 0
metal_gpio_disable_pinmux(gpio_0, 1); // отключение альтернативных функций для вывода 1
Red_V_enable_pullup(0); // включение подтяжки для вывода 0
Red_V_enable_pullup(1); // включение подтяжки для вывода 1
// Конфигурация прерываний
// Level 3: GPIO0_1 Falling Edge Interrupt Enable
Red_V_GPIO_set_ie(1);
Red_V_GPIO_clear_flag(1);
// Level 2: Настройка ПЛИС для GPIO0
Red_V_PLIC_GPIO_set_priority(1,7); // Pin 1, priority 7
// Level 2: ПЛИС (IE1, bit 9) для GPIO0_1
Red_V_PLIC_clear_ie(); // Отключить все остальные прерывания
Red_V_PLIC_set_ie1(9); // Включить GPIO0_1
// Level 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);
// Level 1: Установка базового вектора mtvec
METAL_CPU_SET_CSR(mtvec,gpio_isr));
// Level 1: Включение прерываний с MEIE в mie[11]
METAL_CPU_GET_CSR(mie,saved_config);
saved_config |= (0x1U< <11);
METAL_CPU_SET_CSR(mie,saved_config);
while(1){
if(Red_V_read_pin(0) == 0) // считать входной пин 0
Red_V_clear_pin(5); // Выключить светодиод
}
return 0; // Недоступный код
}
Наконец, мы готовы исправить комбинированное приложение "Мигание и яркость" с помощью прерываний.
Для кнопок, управляющих ШИМ, мы будем продолжать использовать тот же опрос с блокирующими циклами и вызовами функций блокирующей задержки.
Теперь, для части мигания, мы будем использовать периодическое прерывание от устройства PWM2 с частотой 1 мс. Это можно сделать, настроив модуль PWM2 на генерацию сигнала 1 кГц на любом канале, но отключив все его выходы, чтобы можно было использовать пины GPIO, поскольку нас интересует только периодическое прерывание. Сигнал прерывания мы будем использовать для канала 0, который содержит период ШИМ-сигналов, которые будет генерировать модуль. Это ровно 1 миллисекунда.
Каждую миллисекунду обработчик прерывания будет увеличивать счетчик, и когда он достигнет 500, он выключит бортовой светодиод. Когда он достигнет 1000, он включит бортовой светодиод и сбросит его в 0. Это позволит эффективно мигать светодиодом с частотой 1 Гц.
Напомним, что устройство ШИМ позволяет нам генерировать прерывания всякий раз, когда любой из его компараторов выдает 1. То есть, всякий раз, когда таймер достигает своего значения сравнения.
Именно для этого в схеме устройства ШИМ используются сигналы ip. Обратите внимание, что в модулях ШИМ нет регистров разрешения прерываний:
(рис 5.13) Схема каждого ШИМ-устройства в микроконтроллере FE310 (Изображение из руководства пользователя FE310-G002, воспроизведено с разрешения компании SiFive, Inc.)
Для уровней 1 и 2 мы будем использовать тот же код, что и в приложении, управляемом прерываниями GPIO, за исключением строки, в которой мы устанавливаем регистр mtvec для указания на подпрограмму обслуживания прерываний:
// Уровень 1: Установите базовый вектор mtvec METAL_CPU_SET_CSR(mtvec,pwm2_isr);
ШИМ-устройством, используемым для периодического таймера, будет модуль PWM2, и для него мы также будем использовать макросы:
#define Red_V_PWM2_sticky() *((uint32_t *) 0x10035000) |= (1< <8) #define Red_V_PWM2_clear_flag(x) *(( uint32_t *) 0x10035000) = ~(1< <((x)+28))
Опция sticky bits рекомендуется для гарантии того, что прерывания не будут забыты в ожидании выполнения обработчика. Также обратите внимание, что функция Red_V_PWM2_clear_flag() очищает биты ожидания прерывания, записывая в них ноль.
Поскольку в модулях ШИМ нет регистров разрешения прерываний, единственным кодом для конфигурирования прерываний на уровне 3 будет сброс флага:
// Уровень 3: нет регистра ie для каналов ШИМ. Red_V_PWM2_clear_flag(0); // Очистить флаг PWM2_0
Здесь находится код обработчика прерывания ШИМ. Потратьте минуту, чтобы разобраться в нем.
void pwm2_isr(void) __attribute__((interrupt, aligned(64))));
void pwm2_isr(){
static uint32_t count = 0;
uint32_t plic_id;
count++;
// Код мигания if(count == 500) Red_V_clear_pin(5); // Выключите светодиод
if(count == 1000){ Red_V_set_pin(5); // Включите светодиод
count = 0;
}
Red_V_PWM2_clear_flag(0); // Очистить флаг
PWM2_0 plic_id = Red_V_PLIC_claim; // Уровень 2: Прерывание по GPIO Red_V_PLIC_claim = plic_id;
}
Теперь, когда мы увидели многие части кода, пришло время собрать его воедино. Вот код для простого приложения GPIO, управляемого прерываниями. Если вы следите за развитием событий, создайте новый проект на основе проекта примера hello, как обычно, и скопируйте следующий код, чтобы заменить содержимое файла hello.c.
Запустите приложение, чтобы убедиться, что оно работает правильно.
/******************************************************************************
Red-V Thing Plus LED_Interrupts demo,
автор Эдуардо Корпеньо
Использование встроенного светодиода и двух внешних кнопок для управления его яркостью путем опроса, в то время как встроенный светодиод мигает с частотой 1 Гц независимо с помощью прерываний.
Дата: 20 июня 2022 г.
Разработано с использованием Freedom Studio v4.18.0.2021-04-1 на Windows 10
ЛИЦЕНЗИЯ: Этот код выпущен под лицензией MIT License
(http://opensource.org/licenses/MIT)
******************************************************************************/
#include <metal/gpio.h>
#include <metal/pwm.h>
#include <metal.csr.h>
#include <stdint.h>
#include <time.h>
// Макрофункции
#define Red_V_enable_output(x) *((uint32_t *) 0x10012008) |= (1< < (x))
#define Red_V_set_pin(x) *((uint32_t *) 0x1001200C) |= (1< < (x))
#define Red_V_clear_pin(x) *((uint32_t *) 0x1001200C) = ~(1< < (x))
#define Red_V_read_pin(x) (*((uint32_t *) 0x10012000) (1< < (x))))
#define Red_V_enable_pullup(x) *((uint32_t *) 0x10012010) |= (1< < (x))
#define Red_V_enable_DS(x) *((uint32_t *) 0x10012014) |= (1< <(x))
#define Red_V_enable_XOR(x) *((uint32_t *) 0x10012040) |= (1< < (x))
#define Red_V_PWM2_1_set_value(x) *((uint32_t *) 0x10035024) = (x)
#define Red_V_PWM2_sticky() *((uint32_t *) 0x10035000) |= (1< <8)
#define Red_V_PWM2_clear_flag(x) *((uint32_t *) 0x10035000) = ~(1< < ((x)+28))
#define Red_V_PLIC_PWM2_set_priority(ch,p) *((uint32_t *) (0x0C0000C0+4*(ch))) = (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)
void pwm2_isr(void) __attribute__((interrupt, aligned(64))));
void pwm2_isr(){
static uint32_t count = 0;
uint32_t plic_id;
count++;
// Мигающий код
if(count == 500)
Red_V_clear_pin(5); // Выключите бортовой светодиод
if(count == 1000){
Red_V_set_pin(5); // Включите бортовой светодиод
count = 0;
}
Red_V_PWM2_clear_flag(0); // Очистить флаг PWM2_0
plic_id = Red_V_PLIC_claim; // Уровень 2: заявить о прерывании GPIO
Red_V_PLIC_claim = plic_id;
}
// Пользовательская функция задержки
void delay(int number_of_microseconds){
clock_t start_time = clock() // Хранение времени старта
while (clock() < start_time + number_of_microseconds);
}
int main (void) {
unsigned int dc = 50; // Дежурный цикл по умолчанию 50%
struct metal_gpio *gpio_0; // Создаем экземпляр GPIO
struct metal_pwm *pwm_2; // Экземпляр PWM
struct metal_pwm *pwm_1; // Экземпляр ШИМ
gpio_0 = metal_gpio_get_device(0);
// Конфигурация бортового светодиода
metal_gpio_disable_input(gpio_0, 5);
Red_V_enable_output(5);
metal_gpio_disable_pinmux(gpio_0, 5);
metal_gpio_set_pin(gpio_0, 5, 1);
// Конфигурация кнопок
metal_gpio_enable_input(gpio_0, 0); // включить вход 0
metal_gpio_enable_input(gpio_0, 1); // включить вход 1
metal_gpio_disable_output(gpio_0, 0); // отключение вывода 0
metal_gpio_disable_output(gpio_0, 1); // отключение выхода 1
metal_gpio_disable_pinmux(gpio_0, 0); // отключите альтернативные функции для вывода 0
metal_gpio_disable_pinmux(gpio_0, 1); // отключение альтернативных функций для вывода 1
Red_V_enable_pullup(0); // включение подтяжки для вывода 0
Red_V_enable_pullup(1); // включение подтяжки для вывода 1
// Конфигурация ШИМ. Мы будем использовать PWM1_1, который использует GPIO0_19.
Red_V_enable_DS(19); // Высокий уровень сигнала на выходе
Red_V_enable_XOR(19); // Инвертируем выход для активно-высокого ШИМ
pwm_1 = metal_pwm_get_device(1); // Получение экземпляра ШИМ-устройства
while (pwm_1 == NULL); // Остановка. Что-то пошло не так
metal_pwm_enable(pwm_1); // Включить ШИМ1
metal_gpio_disable_pinmux(gpio_0, 20); // Отключить альтернативные функции для пина 20
metal_gpio_disable_pinmux(gpio_0, 21); // Отключить альтернативные функции для вывода 21
metal_gpio_disable_pinmux(gpio_0, 22); // Отключение альтернативных функций для вывода 22
// Установите частоту ШИМ1 на 1 кГц
metal_pwm_set_freq(pwm_1, 1, 1000); // Установите частоту ШИМ1_1 на 1 кГц для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE); // Устанавливаем рабочий цикл для PWM1_1
metal_pwm_trigger(pwm_1, 1, METAL_PWM_CONTINUOUS); // Запуск в непрерывном режиме
// Конфигурация ШИМ. Мы будем использовать PWM2 в качестве периодического прерывания.
pwm_2 = metal_pwm_get_device(2); // Получение экземпляра ШИМ-устройства
while (pwm_2 == NULL); // Остановка. Что-то пошло не так
metal_pwm_enable(pwm_2); // Включить PWM0
metal_gpio_disable_pinmux(gpio_0, 10); // Отключить альтернативные функции для пина 20
metal_gpio_disable_pinmux(gpio_0, 11); // Отключить альтернативные функции для вывода 21
metal_gpio_disable_pinmux(gpio_0, 12); // Отключение альтернативных функций для вывода 22
metal_gpio_disable_pinmux(gpio_0, 13); // Отключение альтернативных функций для вывода 22
// Установите частоту ШИМ2 на 1 кГц
metal_pwm_set_freq(pwm_2, 1, 1000); // Установите частоту ШИМ2_1 на 1 кГц для светодиода
metal_pwm_trigger(pwm_2, 1, METAL_PWM_CONTINUOUS); // Запуск в непрерывном режиме
Red_V_PWM2_sticky();
Red_V_PWM2_1_set_value(8000);
// Конфигурация прерываний
// Уровень 3: нет регистра ie для каналов ШИМ.
Red_V_PWM2_clear_flag(0); // Очистить флаг PWM2_0
// Уровень 2: Настройка PLIC для PWM2
Red_V_PLIC_PWM2_set_priority(0,7); // Канал 0, приоритет 7
// Уровень 2: PLIC (IE2, бит 16) для PWM2_0
Red_V_PLIC_clear_ie(); // Отключите все остальные прерывания
Red_V_PLIC_set_ie2(16); // Включить ШИМ2_0
// Уровень 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: Установка базового вектора mtvec
METAL_CPU_SET_CSR(mtvec,pwm2_isr);
// Уровень 1: включение прерываний с помощью MEIE в mie[11]
METAL_CPU_GET_CSR(mie,saved_config);
saved_config |= (0x1U< <11);
METAL_CPU_SET_CSR(mie,saved_config);
while(1){
// Код ШИМ кнопок и внешнего светодиода
if(Red_V_read_pin(0) == 0){ // Считывание входного пина 0
dc = (dc>0)? dc - 25 : dc; // шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программная задержка
while(Red_V_read_pin(0) == 0); // Ждем, пока кнопка будет отпущена
delay(50000); // Программная задержка
}
else if(Red_V_read_pin(1) == 0){ // Считывание входного пина 1
dc = (dc<100)? dc + 25 : dc; // Шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программная задержка
while(Red_V_read_pin(1) == 0); // Подождите, пока кнопка будет отпущена
delay(50000); // Программная задержка
}
}
return 0; // Недоступный код
}
Поздравляю! Вы, должно быть, многому научились только в этой лекции.
Давайте вспомним, что мы рассмотрели в этой лекции:
Не стесняйтесь возвращаться к этому материалу, если в будущем вам понадобится подтянуть знания.
В этой лекции мы поговорим об эффективной обработке ввода-вывода. Сначала мы поговорим о двух методах обработки ввода-вывода, называемых опросом и прерываниями. Затем мы создадим простое, но ужасное GPIO-приложение, просто чтобы показать вам, насколько плохим может быть опрос. Для того чтобы решить эту проблему, нам придется узнать, как обычно работают прерывания, затем как они работают в микроконтроллере FE310. А после этого, мы создадим простое приложение, управляемое прерываниями GPIO, чтобы познакомить вас с процессом обработки прерываний. И после того, как мы создадим это приложение, мы сможем перейти к исправлению нашего примера опроса с помощью прерываний. Вы увидите, что эта замена приложения намного лучше, чем первая попытка. Нам еще многому предстоит научиться.
Так что давайте приступим к делу.
К концу этой лекции вы должны:
Существует несколько подходов к работе с устройствами ввода/вывода. Мы использовали прямой подход, называемый опросом, поскольку он заключается в опросе всех используемых устройств ввода-вывода в поисках того, которое требует нашего внимания (внимания процессора). Именно этим занимались наши приложения в бесконечном главном цикле.
Более эффективная техника обработки ввода-вывода называется обработкой прерываний. Она использует уникальный аппаратный элемент в ядре процессора, который выполняет опрос за вас (грубо говоря). Всякий раз, когда устройство ввода-вывода требует внимания процессора, логика этого устройства устанавливает бит, который запускает процесс прерывания
Когда происходит прерывание, центральный процессор останавливает выполнение кода, который выполнялся в данный момент, сохраняет часть состояния процессора в стеке и переходит к выполнению функции обратного вызова, известной как подпрограмма обслуживания прерывания (Interrupt Service Routine, ISR), или обработчик прерывания.
Каждое поддерживаемое прерывание имеет свой обработчик прерывания. Задача программиста - написать эти обработчики прерываний с кодом, который будет либо принимать входящие данные, либо отправлять исходящие данные. После выполнения обработчика прерывания, вместо того чтобы вернуться в виде вызванной функции, процессор возобновляет работу, которую он выполнял, когда был прерван. Обработка прерываний - это высокоэффективная техника обработки ввода-вывода, в то время как опрос влечет за собой ненужные накладные расходы, которые обычно впустую расходуют наше драгоценное процессорное время. Именно поэтому опрос обычно делает пользовательские интерфейсы менее отзывчивыми, а вычисления медленными.
Давайте рассмотрим мысленный эксперимент для лучшего понимания опроса и обработки прерываний. Предположим, вы отправились в торговый центр, потому что вам нужно купить несколько вещей в разных магазинах. Прежде чем отправиться в магазины, вы заходите в фуд-корт и заказываете еду, которая займет некоторое время. Вы не знаете, сколько времени потребуется на приготовление. Это может быть от 10 до 45 минут. Как же вы собираетесь делать покупки?
Подход, основанный на опросе
Подход, основанный на опросе, можно сравнить с походом в магазин за покупками на 5 минут, а затем возвращением в фуд-корт и вопросом о вашем блюде. Если оно еще не готово, тогда вы идете за покупками на 5 минут, возвращаетесь в фуд-корт и спрашиваете снова. Так продолжается до тех пор, пока ваше блюдо не будет готово. Когда оно готово, вы садитесь и едите. Затем вы можете продолжить покупки.
Подход, основанный на прерываниях
Подход, основанный на прерываниях, похож на то, как если бы вы оставили номер своего мобильного телефона, чтобы персонал ресторана позвонил вам, когда ваше блюдо будет готово. Когда вам позвонят, вы должны будете прекратить делать то, что делали в этот момент, и пойти в фуд-корт, чтобы поесть. После еды вы можете вернуться в магазин и продолжить начатое. Таким образом, вы можете ходить за покупками без всякого беспокойства.
Какой подход лучше?
Теперь мы добавим несколько чисел, чтобы нам было легче провести сравнение. Это будет очень упрощенно: допустим, что путь от магазина до фуд-корта занимает 2 минуты (таким образом, путь туда и обратно займет 4 минуты), и что в итоге еда будет готова через 30 минут после заказа.
При подходе, основанном на опросе, ваше драгоценное время будет использовано следующим образом: через 34 минуты вы потратите на покупки 20 минут, а на вопросы о еде - 14 минут.
(рис 5.1) Использование времени при голосовании за еду в торговом центре
Эффективность
Эффективность - одна из немногих метрик, которые волнуют инженеров. Она означает, насколько эффективно используется ограниченный ресурс. Стремление к максимизации эффективности эквивалентно стремлению к минимизации отходов. Существует столько же форм измерения эффективности, сколько и ограниченных ресурсов. Эффективность часто обозначается греческой буквой eta ($$\eta$$).
Если мы определим эффективность как процент от общего времени, которое вы потратили на полезную работу (покупки), то в данном случае эффективность будет следующей:
$$\eta_{Tine}=\frac{t_{Shopping}}{t_{Total}}*100%=\frac{20minutes}{34minutes}*100%\approx59%$$Эффективность также можно рассчитать, как количество посещений фуд-корта, которые оказались полезными, из общего числа посещений:
$$\eta_{Visits}=\frac{Visits_{Useful}}{Visits_{Total}}*100%=\frac{1visit}{4visits}*100%\approx25%$$При подходе, основанном на прерывании, вы потратите 30 минут на покупки, плюс время, необходимое для того, чтобы ответить на звонок и забрать продукты:
(рис 5.2) Использование времени при ожидании телефонного звонка по поводу еды в торговом центре
Рассчитывая те же показатели эффективности для подхода с прерыванием, мы получаем следующее:
| Метод опроса | Метод прерываний | |
|---|---|---|
| Эффективность использования времени | 59% | 94% |
| Эффективность визитов на фудкорт | 25% | 100% |
Ускорение
Еще одна метрика - Ускорение, которая измеряет, насколько быстрее работает один вариант по сравнению с другим. К тому времени, когда вы получите еду, вы проведете за покупками 20 минут в случае с методом опроса, и 30 минут в случае использования прерываний. Таким образом, если сравнивать варианты по истечении 32 минут, то ускорение при использовании прерываний по сравнению с методом опроса составит:
$$Speedup=\frac{Time_{Polling}}{Time_{Interrupts}}=\frac{30minutes}{20minutes}=1.5$$Другими словами, с помощью прерываний работа выполняется в 1,5 раза быстрее по сравнению с методом опросом в данном примере.
Теперь рассмотрим встроенную систему. Здесь мы имеем код опроса для автомобильного приложения, которое управляет приборной панелью в автомобиле. Предположим, что все функции являются блокирующими, и каждой из них требуется время, указанное в комментарии справа, чтобы вернуться.
void main(){
while(1){
read_touch_screen(); // 100ms
read_GPS(); // 300ms
read_fuel_level(); // 200ms
read_velocity(); // 200ms
read_temperature(); // 100ms
refresh_door_light(); // 100ms
refresh_display(); // 200ms
}
}
Очевидно, что это приложение демонстрирует чрезмерное использование кода в главном цикле. Обратите внимание, что первая функция считывает данные с сенсорного экрана, а последняя обновляет дисплей. Вся работа между ними, безусловно, сделает приложение неотзывчивым. Также обратите внимание, что нет необходимости проверять каждое устройство ввода-вывода на каждой отдельной итерации главного цикла. Вот как выглядит выполнение с течением времени:
(рис 5.3) Хронология исполнения кода приложения приборной панели при использовании подхода опроса
Теперь давайте рассмотрим альтернативу, управляемую прерываниями.
void touch_screen_ISR(); // 100ms
void GPS_ISR(); // 300ms
void fuel_level_ISR(); // 200ms
void velocity_ISR(); // 200ms
void temperature_ISR(); // 100ms
void door_light_ISR(); // 100ms
void main(){
while(1)
refresh_display(); // 200ms
}
Обратите внимание, что в главном цикле вызывается только функция обновления дисплея. Остальные функции заменены обработчиками прерываний, также известными как подпрограммы обслуживания прерываний (ISR). Эти ISR занимают столько же времени, сколько и их аналоги, но они выполняются только тогда, когда возникает необходимость (открывается дверь, пользователь касается экрана, меняется уровень топлива и так далее). Это ответы на запросы ввода-вывода, а не команды на чтение или вывод чего-либо, как в подходе с опросами.
Необходимым условием эффективной работы прерываний является то, что запросы на прерывание должны поступать с гораздо меньшей частотой, чем запросы основного потока. Другими словами, прерывания должны возникать не постоянно, а время от времени, иначе они будут мешать выполнению основной задачи. Все, что постоянно требует внимания, должно находиться в основном цикле.
Здесь мы имеем возможную временную шкалу примера с прерыванием:
(рис 5.4) Хронология исполнения кода приложения приборной панели при использовании прерываний
Поскольку запросы ввода-вывода происходят нечасто, основной цикл может работать без сбоев.
Возможно, вы не до конца уверены, что прерывания всегда лучше, чем опрос, поэтому давайте посмотрим, насколько плохим может быть приложение с опросом, если оно плохо спроектировано. Опросом можно злоупотреблять до такой степени, что несколько задач будут конкурировать за процессорное время, что приведет к плохому выполнению общей задачи.
Простым примером для иллюстрации этого является объединение двух приложений, которые мы уже видели ранее: Проект Blinky и Демонстрация управления яркостью светодиодов. Исходный код этих приложений будет объединен в один файл C. Это объединенное приложение будет использовать те же входные переключатели и светодиоды, что и демонстрация ШИМ. Вот принципиальная схема:
(рис 5.5) Принципиальная схема демонстрационного приложения, использующего метод опроса
Кнопка 0 уменьшит яркость внешнего светодиода на ступень, а кнопка 1 увеличит ее на ступень.
На этот раз внешний светодиод будет иметь 5 уровней яркости, увеличивающихся с шагом в 25%, поэтому он также будет переходить от 0% к 100%. Тем временем, встроенный светодиод будет мигать с частотой 1 Гц независимо.
Это контакты ввода/вывода, которые мы будем использовать для подключения кнопок и внешнего светодиода:
(рис 5.6) Виды сверху и снизу Red-V Thing Plus, показывающие контакты, которые будут использоваться в демонстрационном приложении для опроса (Изображение взято с сайта SparkFun Electronics, по лицензии CC BY 2.0)
Здесь представлены две фотографии фактических аппаратных соединений:
(рис 5.7) Аппаратные соединения для приложения управления светодиодом, основанном на методе опроса
Наша первая попытка использует простую адаптацию кода на языке C для обоих приложений, используя подход опроса.
Здесь приведен код приложения для опроса светодиодов. Если вы следите за развитием событий, создайте новый проект на основе проекта примера hello, как обычно, и скопируйте следующий код, чтобы заменить содержимое файла hello.c.
/******************************************************************************
Red-V Thing Plus LED_Polling demo,
by Eduardo Corpe?o
Using the built-in LED and two external buttons to control its brightness,
while the on-board LED blinks at 1Hz independently.
Date: June 20, 2022
Developed using Freedom Studio v4.18.0.2021-04-1 on Windows 10
LICENSE: This code is released under the MIT License
(http://opensource.org/licenses/MIT)
******************************************************************************/
#include <metal/gpio.h>
#include <metal/pwm.h>
#include <stdint.h>
#include <time.h>
#define Red_V_enable_output(x) * ((uint32_t * ) 0x10012008) |= (1 << (x))
#define Red_V_set_pin(x) * ((uint32_t * ) 0x1001200C) |= (1 << (x))
#define Red_V_clear_pin(x) * ((uint32_t * ) 0x1001200C) = ~(1 << (x))
#define Red_V_read_pin(x)( * ((uint32_t * ) 0x10012000) (1 << (x))))
#define Red_V_enable_pullup(x) * ((uint32_t * ) 0x10012010) |= (1 << (x))
#define Red_V_enable_DS(x) * ((uint32_t * ) 0x10012014) |= (1 << (x))
#define Red_V_enable_XOR(x) * ((uint32_t * ) 0x10012040) |= (1 << (x))
// Пользовательская функция задержки
void delay(int number_of_microseconds) {
clock_t start_time = clock(); // Хранение времени запуска
while (clock() < start_time + number_of_microseconds);
}
int main(void) {
unsigned int dc = 50; // По умолчанию рабочий цикл равен 50%
struct metal_gpio * gpio_0; // Экземпляр GPIO
struct metal_pwm * pwm_1; // Экземпляр PWM
gpio_0 = metal_gpio_get_device(0);
// Конфигурация встроенного светодиода
metal_gpio_disable_input(gpio_0, 5);
Red_V_enable_output(5);
metal_gpio_disable_pinmux(gpio_0, 5);
metal_gpio_set_pin(gpio_0, 5, 1);
// Конфигурация кнопок
metal_gpio_enable_input(gpio_0, 0); // включить ввод 0
metal_gpio_enable_input(gpio_0, 1); // включить ввод 1
metal_gpio_disable_output(gpio_0, 0); // отключить вывод 0
metal_gpio_disable_output(gpio_0, 1); // отключить вывод 1
metal_gpio_disable_pinmux(gpio_0, 0); // отключить альтернативные функции для пина 0
metal_gpio_disable_pinmux(gpio_0, 1); // // отключить альтернативные функции для пина 1
Red_V_enable_pullup(0); // включить подтягивающий резистор для пина 0
Red_V_enable_pullup(1); // включить подтягивающий резистор для пина 1
// Конфигурация ШИМ. Мы будем использовать PWM1_1, который использует GPIO0_19.
Red_V_enable_DS(19); // Сила высокого выхода
Red_V_enable_XOR(19); // Инвертируем выход для активно-высокого ШИМа
pwm_1 = metal_pwm_get_device(1); // Получение экземпляра ШИМ-устройства
while (pwm_1 == NULL); // Остановка.Что-то пошло не так
metal_pwm_enable(pwm_1); // Включить ШИМ1
metal_gpio_disable_pinmux(gpio_0, 20); // Отключить альтернативные функции для пина 20
metal_gpio_disable_pinmux(gpio_0, 21); // Отключить альтернативные функции для пина 21
metal_gpio_disable_pinmux(gpio_0, 22); // Отключить альтернативные функции для пина 22
// Установите частоту ШИМ1 на 1 кГц
metal_pwm_set_freq(pwm_1, 1, 1000); // Установите частоту ШИМ1_1 на 1 кГц для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE); // Устанавливаем рабочий цикл для ШИМ1_1
metal_pwm_trigger(pwm_1, 1, METAL_PWM_CONTINUOUS);
// Запуск в непрерывном режиме
while (1) {
// Кнопки и код ШИМ внешнего светодиода
if (Red_V_read_pin(0) == 0) { // Чтение входного пина 0
dc = (dc > 0) ? dc - 25 : dc; // Шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc,
METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программное ожидание
while (Red_V_read_pin(0) == 0); // Ждем, пока кнопка не будет отпущена
delay(50000); // Программное ожидание
} else if (Red_V_read_pin(1) == 0) { // Считывание входного пина 1
dc = (dc < 100) ? dc + 25 : dc; // Шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программное ожидание
while (Red_V_read_pin(1) == 0); // Ждем, пока кнопка будет отпущена
delay(50000); // Программное ожидание
}
// Код мигающего светодиода
Red_V_clear_pin(5); // Выключение встроенного светодиода
delay(500000); // 500 мс в микросекундах
Red_V_set_pin(5); // Включение встроенного светодиода
delay(500000); // 500 мс в микросекундах
}
return 0; // Недостижимый код
}
Обратите внимание на следующие детали в коде:
delay(), ту, которая вызывает функцию clock() из time.h. Мы не используем быструю и грязную функцию задержки, использованную ранее для предотвращения дребезга кнопок.Попробуйте приложение и проверьте, правильно ли оно работает. Спойлер: это не так!
Чтобы ответить на этот вопрос, давайте обратимся к части кода, посвященной опросу, в главном цикле:
while (1) {
// Кнопки и код ШИМ внешнего светодиода
if (Red_V_read_pin(0) == 0) { // Чтение входного пина 0
dc = (dc > 0) ? dc - 25 : dc; // Шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc,
METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программное ожидание
while (Red_V_read_pin(0) == 0); // Ждем, пока кнопка не будет отпущена
delay(50000); // Программное ожидание
} else if (Red_V_read_pin(1) == 0) { // Считывание входного пина 1
dc = (dc < 100) ? dc + 25 : dc; // Шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программное ожидание
while (Red_V_read_pin(1) == 0); // Ждем, пока кнопка будет отпущена
delay(50000); // Программное ожидание
}
// Код мигающего светодиода
Red_V_clear_pin(5); // Выключение встроенного светодиода
delay(500000); // 500 мс в микросекундах
Red_V_set_pin(5); // Включение встроенного светодиода
delay(500000); // 500 мс в микросекундах
}
Обратите внимание, что везде присутствуют задержки, отнимающие время. Проблема в том, что функция задержки - это так называемая блокирующая функция, которая блокирует выполнение до тех пор, пока время не истечет.
Еще один блокирующий элемент можно увидеть в циклах while между задержками в строках кода для предотвращения дребезга. Проблема заключается в том, что приложение не будет выполняться до тех пор, пока кнопка не будет отпущена. Это не является проблемой для части ШИМ, потому что генератор ШИМ - это отдельная часть аппаратного обеспечения, которая работает независимо. Тем не менее, код мигающего светодиода нуждается в процессоре.
Альтернативой блокирующим функциям являются неблокирующие функции. Когда вызывается неблокирующая функция, она вызывается кратко перед возвратом. Если же она должна дождаться выполнения внешней задачи, она возвращается, откладывая обработку результата этой задачи.
Неблокирующая функция задержки может запустить таймер и вернуться. Затем остальная часть кода может использовать опрос для проверки выполнения таймера, не задерживая выполнение в блокирующем цикле.
При разумном использовании в опросе нет ничего плохого. Однако опросом можно злоупотреблять несколькими способами, например, блокировать функции, добавлять слишком много кода в основной цикл или без необходимости прерывать выполнение основной задачи. Именно тогда он становится проблемой.
Прерывания подходят для обработки нечастых запросов ввода-вывода, таких как нажатие клавиш или спорадические входящие сообщения от другого устройства. В целом, обработчики прерываний должны быть намного быстрее, чем скорость поступления прерываний.
Если это условие выполняется, то прерывания помогают сделать встроенные приложения лучше во многих отношениях. Давайте рассмотрим некоторые из этих преимуществ:
Прерывания всегда присутствуют в профессиональных приложениях, но они не очень популярны в сообществе Arduino, возможно, из-за кажущейся сложности процесса по сравнению с простым написанием кода обработки ввода-вывода в основном цикле, что, как мы знаем, является плохой практикой. Если вы решили никогда не использовать прерывания в своих приложениях, то создание программ-игрушек - это максимум, на что вы способны. Если вы серьезно занимаетесь встраиваемыми системами, прерывания, рано или поздно, должны быть добавлены в ваш инструментарий.
Все микроконтроллеры поддерживают прерывания, и схема может варьироваться от очень простой государственной машины в CPU до очень сложного внешнего блока.
Важно понимать, что обработка прерываний - это аппаратный, а не программный процесс. Как программист, вы не должны беспокоиться об этом процессе, но должны предоставить всю информацию, необходимую аппаратуре для правильной обработки прерываний. По этой причине вы должны знать основные элементы, используемые в контроллере прерываний.
Контроллеры прерываний в микроконтроллерах реализуют некоторые варианты следующих аппаратных средств:
(рис 5.8) Типичное оборудование контроллера прерываний
Обратите внимание на следующие детали схемы:
Когда сигнал прерывания достигает центрального процессора, запускается процесс подготовки к обработке прерывания:
Мотивация использования подхода, основанного на прерываниях, заключается в том, чтобы избежать остановки выполнения кода с помощью блокирующих функций или блокирующих циклов, поэтому следует, что обработчики прерываний должны быть краткими.
Основное приложение было прервано (подумайте об этом, как о том, чтобы заставить кого-то выйти с совещания), поэтому ваш код должен перейти прямо к делу. В обработчике прерывания есть место только для срочного кода. Все остальное должно быть отложено до главного цикла. Поэтому общее эмпирическое правило - избегать длинных вычислений в обработчиках прерываний (ISR).
Кроме того, нет ничего хорошего в том, чтобы пройти через все трудности разработки приложения, управляемого прерываниями только для того, чтобы написать блокирующие подпрограммы обслуживания прерываний. Даже случайно. Поэтому, помимо отказа от использования блокирующего кода, еще одним общим правилом при написании обработчиков прерываний является отказ от использования циклов. Всегда пишите код обработчика прерываний без циклов, если это возможно.
Для получения дополнительных советов по написанию обработчиков прерываний, вы можете прочитать статью в блоге Embedded Artistry.
Before diving into the interrupt usage process for the FE3190, here is an attempt to not scare you away by looking at the big picture.
Прежде чем погрузиться в процесс использования прерываний для FE3190, здесь мы попытаемся не отпугнуть вас, взглянув на общую картину.
Оказывается, FE310 использует несколько уровней одного и того же процесса, который мы только что видели, потому что у него есть несколько уровней контроллеров прерываний. Это происходит потому, что RISC-V описывает использование прерываний только в своей спецификации режимов привилегий. За пределами этой спецификации производители должны реализовать свои контроллеры прерываний и свои модули ввода/вывода, которые имеют некоторые шаги конфигурации для прерываний.
Микроконтроллер FE310 использует 3 уровня сигналов разрешения/отсрочки прерывания, и весь процесс может быть запутанным из-за большого количества регистров и лишних шагов. Чтобы упростить ситуацию, на следующей схеме приведены регистры, участвующие в интересующем нас процессе:
(рис 5.9) Регистры обработки прерываний для устройств ввода/вывода в микроконтроллере FE310
Ядро процессора обслуживает только 3 возможных источника прерываний: MEI, MSI и MTI. Нас интересует MEI, машинное внешнее прерывание. Вот регистры процессора, показанные на схеме (они не привязаны к памяти):
Этот контроллер отвечает за так называемые глобальные прерывания, которые поступают от устройств ввода/вывода (GPIO, PWM, SPI, I2C и т.д.). Вот интересующие нас регистры с привязкой к памяти:
enable1, а старшая половина - enable2.0x0C000004 - 0x0C0000D0.Различные устройства ввода/вывода могут по-разному реализовывать свои регистры прерываний, но обычно они имеют следующие регистры, привязанные к памяти:
pwmcfg.Микроконтроллер FE310 поддерживает несколько типов прерываний. Другими словами, прерывания могут поступать из нескольких различных источников. На следующей диаграмме показана архитектура прерываний FE310. Обратите внимание, что есть два контроллера прерываний:
(рис 5.10) Архитектура прерываний FE310 (Изображение из руководства пользователя FE310-G002, воспроизведено с разрешения компании SiFive, Inc.)
Platform-Level Interrupt Controller (ПЛИС) - это глобальный контроллер прерываний в системе RISC-V. Этот контроллер обеспечивает так называемый сигнал Machine External Interrupt ядру E31. Как показано на схеме, этот контроллер отвечает за внешние устройства ввода/вывода.
Core-Local Interruptor (CLINT) генерирует локальные прерывания, связанные с определенными аппаратными потоками (или сокращенно harts). Прерывания, обеспечиваемые этим контроллером, - это прерывание машинного программного обеспечения и прерывание машинного таймера.
Далее мы рассмотрим более подробное описание регистров, участвующих в настройке и использовании прерываний.
Эти три типа прерываний называются машинными, поскольку они работают в режиме привилегий RISC-V, называемом машинным режимом, который имеет полный доступ к памяти, вводу/выводу и низкоуровневым функциям. Машинный режим - это самый привилегированный режим процессора RISC-V. Обычные потоки, такие как главная функция в ваших программах, работают в режиме пользователя, который предназначен для потоков с ограниченными привилегиями.
Машинный режим RISC-V полагается на 8 регистров управления и состояния (CSR) для обработки исключений и прерываний. Это первый уровень регистров прерываний, с которыми мы должны работать.
Здесь приведено описание КСО (нам не обязательно использовать все из них):
Как получить доступ к КСО
КСО не отображаются в памяти, поэтому в базовом целочисленном ISA есть 6 специальных инструкций для доступа к этим регистрам. Это более 12% набора команд, поэтому следует, что эти команды должны быть важными.
Единственным способом доступа к этим регистрам являются инструкции CSRRW{I}, CSRRS{I} и CSRRC{I}. Не волнуйтесь, инструментарий компилятора позволяет встраивать инструкции ассемблера в код на языке Си.
Нас интересует использование Контроллера прерывания на уровне платформы (ПЛИС) для прерываний от внешних устройств. Это второй уровень регистров прерываний, с которыми мы должны работать.
Как мы видели на принципиальной схеме ПЛИС имеет 52 источника. Это все контакты, подключенные к модулям ввода/вывода, включая их функции GPIO и аппаратного ввода/вывода. Нумерацию этих источников прерываний мы можем найти в таблице 26 руководства FE310:
(рис 5.11) Сопоставление источников прерываний ПЛИС в микроконтроллере FE310 (Изображение из руководства пользователя FE310-G002, воспроизведено с разрешения компании SiFive, Inc)
Для обработки запросов ввода-вывода в модуле ПЛИС мы будем использовать следующие регистры, привязанные к памяти:
Как и большинство микроконтроллеров, FE310 оснащен множеством устройств ввода/вывода. Кроме принципа работы эти устройства отличаются способом реализации прерываний:
Вспомните регистры GPIO из таблицы 52 руководства пользователя:
(рис 5.12) Смещение и описание регистров конфигурации GPIO (Изображение из руководства пользователя FE310-G002, воспроизведено с разрешения компании SiFive, Inc.)
Обратите внимание, что в адресах с 0x18 по 0x34 находятся регистры разрешения прерывания и ожидания прерывания. Как уже упоминалось, они предназначены для генерации прерываний в случаях падающего фронта, нарастающего фронта, высокого состояния и низкого состояния. Как и другие регистры модуля GPIO, эти регистры разрешают прерывания или указывают на ожидающие прерывания, относящиеся ко всем 32 выводам модуля GPIO, побитово.
Прежде чем исправлять наше неисправное приложение "Мигание и яркость", давайте начнем с более простого приложения, чтобы ознакомить вас с процессом.
Вспомните, как работает наш первый демонстрационный вход GPIO: Кнопка 0 выключает светодиод на плате, а кнопка 1 включает его, вот так:
while(1){
if(Red_V_read_pin(0) == 0) // Считать входной пин 0
Red_V_clear_pin(5); // Выключить светодиод
else if(Red_V_read_pin(1) == 0) // Считать входной пин 1
Red_V_set_pin(5); // Включить светодиод
}
Затем измените это приложение, чтобы заменить обработку кнопки 1 на управление прерыванием.
Для этого приложения можно использовать то же оборудование, которое мы используем для приложения "Мигание и яркость".
В дальнейшем мы не будем использовать библиотеку 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 соответственно, поэтому все операции первого уровня будут выполняться с помощью этих макросов.
Вот как мы можем установить разрешение глобального прерывания (бит 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.
Обработчики прерываний могут быть как подпрограммами ассемблера, так и функциями языка Си, но к ним предъявляются два особых требования:
В языке 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
Обратите внимание на следующие детали:
Тело 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
Обратите внимание на следующие детали:
Теперь, когда мы увидели многие части кода, пришло время собрать его воедино. Вот код для простого приложения GPIO, управляемого прерываниями. Если вы следите за развитием событий, создайте новый проект на основе проекта примера hello, как обычно, и скопируйте следующий код, чтобы заменить содержимое файла hello.c.
Запустите приложение, чтобы убедиться, что оно работает правильно.
/******************************************************************************
Red-V Thing Plus Interrupt_Demo,
автор Эдуардо Корпеньо
Для управления используется встроенный светодиод и две внешние кнопки.
Одна кнопка управляется опросом, а другая - прерываниями.
Дата: 20 июня 2022 г.
Разработано с использованием Freedom Studio v4.18.0.2021-04-1 на Windows 10
ЛИЦЕНЗИЯ: Этот код выпущен под лицензией MIT License
(http://opensource.org/licenses/MIT)
******************************************************************************/
#include <metal/gpio.h> //include GPIO library, https://sifive.github.io/freedom-metal-docs/apiref/gpio.html
#include <stdint.h>
#include <metal/csr.h>
// Macro Functions
#define Red_V_enable_output(x) *((uint32_t *) 0x10012008) |= (1< <(x))
#define Red_V_set_pin(x) *((uint32_t *) 0x1001200C) |= (1<< (x))
#define Red_V_clear_pin(x). *((uint32_t *) 0x1001200C) = ~(1< <(x))
#define Red_V_read_pin(x) (*((uint32_t *) 0x10012000) (1< <(x))
#define Red_V_enable_pullup(x) *((uint32_t *) 0x10012010) |= (1< <(x))
#define Red_V_GPIO_set_ie(x) *((uint32_t *) 0x10012020) |= (1< <(x))
#define Red_V_GPIO_clear_flag(x) *((uint32_t *) 0x10012024) |= (1< <(x))
#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)
void gpio_isr(void) __attribute__((interrupt, aligned(64)));
void gpio_isr(){
uint32_t plic_id;
Red_V_set_pin(5); //Включить светодиод
Red_V_GPIO_clear_flag(1); // Level 3: Очистить флаг GPIO0_1
plic_id = Red_V_PLIC_claim; // Level 2: Объявить прерывание GPIO
Red_V_PLIC_claim = plic_id;
}
int main (void){
struct metal_gpio *gpio_0; // Создать экземпляр GPIO
gpio_0 = metal_gpio_get_device(0);
// Пины устанавливаются при инициализации, поэтому мы должны отключить его, когда используем его как вход/выход
metal_gpio_disable_input(gpio_0, 5);
// Устанавливаем gpio как выход
//metal_gpio_enable_output(gpio_0, 5);
Red_V_enable_output(5);
// Пины имеют более одной функции, убедитесь, что мы отключаем все подключенное
metal_gpio_disable_pinmux(gpio_0, 5);
metal_gpio_enable_input(gpio_0, 0); // включить вход 0
metal_gpio_enable_input(gpio_0, 1); // включить вход 1
metal_gpio_disable_output(gpio_0, 0); // отключить вывод 0
metal_gpio_disable_output(gpio_0, 1); // отключить вывод 1
metal_gpio_disable_pinmux(gpio_0, 0); // отключение альтернативных функций для вывода 0
metal_gpio_disable_pinmux(gpio_0, 1); // отключение альтернативных функций для вывода 1
Red_V_enable_pullup(0); // включение подтяжки для вывода 0
Red_V_enable_pullup(1); // включение подтяжки для вывода 1
// Конфигурация прерываний
// Level 3: GPIO0_1 Falling Edge Interrupt Enable
Red_V_GPIO_set_ie(1);
Red_V_GPIO_clear_flag(1);
// Level 2: Настройка ПЛИС для GPIO0
Red_V_PLIC_GPIO_set_priority(1,7); // Pin 1, priority 7
// Level 2: ПЛИС (IE1, bit 9) для GPIO0_1
Red_V_PLIC_clear_ie(); // Отключить все остальные прерывания
Red_V_PLIC_set_ie1(9); // Включить GPIO0_1
// Level 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);
// Level 1: Установка базового вектора mtvec
METAL_CPU_SET_CSR(mtvec,gpio_isr));
// Level 1: Включение прерываний с MEIE в mie[11]
METAL_CPU_GET_CSR(mie,saved_config);
saved_config |= (0x1U< <11);
METAL_CPU_SET_CSR(mie,saved_config);
while(1){
if(Red_V_read_pin(0) == 0) // считать входной пин 0
Red_V_clear_pin(5); // Выключить светодиод
}
return 0; // Недоступный код
}
Наконец, мы готовы исправить комбинированное приложение "Мигание и яркость" с помощью прерываний.
Для кнопок, управляющих ШИМ, мы будем продолжать использовать тот же опрос с блокирующими циклами и вызовами функций блокирующей задержки.
Теперь, для части мигания, мы будем использовать периодическое прерывание от устройства PWM2 с частотой 1 мс. Это можно сделать, настроив модуль PWM2 на генерацию сигнала 1 кГц на любом канале, но отключив все его выходы, чтобы можно было использовать пины GPIO, поскольку нас интересует только периодическое прерывание. Сигнал прерывания мы будем использовать для канала 0, который содержит период ШИМ-сигналов, которые будет генерировать модуль. Это ровно 1 миллисекунда.
Каждую миллисекунду обработчик прерывания будет увеличивать счетчик, и когда он достигнет 500, он выключит бортовой светодиод. Когда он достигнет 1000, он включит бортовой светодиод и сбросит его в 0. Это позволит эффективно мигать светодиодом с частотой 1 Гц.
Напомним, что устройство ШИМ позволяет нам генерировать прерывания всякий раз, когда любой из его компараторов выдает 1. То есть, всякий раз, когда таймер достигает своего значения сравнения.
Именно для этого в схеме устройства ШИМ используются сигналы ip. Обратите внимание, что в модулях ШИМ нет регистров разрешения прерываний:
(рис 5.13) Схема каждого ШИМ-устройства в микроконтроллере FE310 (Изображение из руководства пользователя FE310-G002, воспроизведено с разрешения компании SiFive, Inc.)
Для уровней 1 и 2 мы будем использовать тот же код, что и в приложении, управляемом прерываниями GPIO, за исключением строки, в которой мы устанавливаем регистр mtvec для указания на подпрограмму обслуживания прерываний:
// Уровень 1: Установите базовый вектор mtvec METAL_CPU_SET_CSR(mtvec,pwm2_isr);
ШИМ-устройством, используемым для периодического таймера, будет модуль PWM2, и для него мы также будем использовать макросы:
#define Red_V_PWM2_sticky() *((uint32_t *) 0x10035000) |= (1< <8) #define Red_V_PWM2_clear_flag(x) *(( uint32_t *) 0x10035000) = ~(1< <((x)+28))
Опция sticky bits рекомендуется для гарантии того, что прерывания не будут забыты в ожидании выполнения обработчика. Также обратите внимание, что функция Red_V_PWM2_clear_flag() очищает биты ожидания прерывания, записывая в них ноль.
Поскольку в модулях ШИМ нет регистров разрешения прерываний, единственным кодом для конфигурирования прерываний на уровне 3 будет сброс флага:
// Уровень 3: нет регистра ie для каналов ШИМ. Red_V_PWM2_clear_flag(0); // Очистить флаг PWM2_0
Здесь находится код обработчика прерывания ШИМ. Потратьте минуту, чтобы разобраться в нем.
void pwm2_isr(void) __attribute__((interrupt, aligned(64))));
void pwm2_isr(){
static uint32_t count = 0;
uint32_t plic_id;
count++;
// Код мигания if(count == 500) Red_V_clear_pin(5); // Выключите светодиод
if(count == 1000){ Red_V_set_pin(5); // Включите светодиод
count = 0;
}
Red_V_PWM2_clear_flag(0); // Очистить флаг
PWM2_0 plic_id = Red_V_PLIC_claim; // Уровень 2: Прерывание по GPIO Red_V_PLIC_claim = plic_id;
}
Теперь, когда мы увидели многие части кода, пришло время собрать его воедино. Вот код для простого приложения GPIO, управляемого прерываниями. Если вы следите за развитием событий, создайте новый проект на основе проекта примера hello, как обычно, и скопируйте следующий код, чтобы заменить содержимое файла hello.c.
Запустите приложение, чтобы убедиться, что оно работает правильно.
/******************************************************************************
Red-V Thing Plus LED_Interrupts demo,
автор Эдуардо Корпеньо
Использование встроенного светодиода и двух внешних кнопок для управления его яркостью путем опроса, в то время как встроенный светодиод мигает с частотой 1 Гц независимо с помощью прерываний.
Дата: 20 июня 2022 г.
Разработано с использованием Freedom Studio v4.18.0.2021-04-1 на Windows 10
ЛИЦЕНЗИЯ: Этот код выпущен под лицензией MIT License
(http://opensource.org/licenses/MIT)
******************************************************************************/
#include <metal/gpio.h>
#include <metal/pwm.h>
#include <metal.csr.h>
#include <stdint.h>
#include <time.h>
// Макрофункции
#define Red_V_enable_output(x) *((uint32_t *) 0x10012008) |= (1< < (x))
#define Red_V_set_pin(x) *((uint32_t *) 0x1001200C) |= (1< < (x))
#define Red_V_clear_pin(x) *((uint32_t *) 0x1001200C) = ~(1< < (x))
#define Red_V_read_pin(x) (*((uint32_t *) 0x10012000) (1< < (x))))
#define Red_V_enable_pullup(x) *((uint32_t *) 0x10012010) |= (1< < (x))
#define Red_V_enable_DS(x) *((uint32_t *) 0x10012014) |= (1< <(x))
#define Red_V_enable_XOR(x) *((uint32_t *) 0x10012040) |= (1< < (x))
#define Red_V_PWM2_1_set_value(x) *((uint32_t *) 0x10035024) = (x)
#define Red_V_PWM2_sticky() *((uint32_t *) 0x10035000) |= (1< <8)
#define Red_V_PWM2_clear_flag(x) *((uint32_t *) 0x10035000) = ~(1< < ((x)+28))
#define Red_V_PLIC_PWM2_set_priority(ch,p) *((uint32_t *) (0x0C0000C0+4*(ch))) = (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)
void pwm2_isr(void) __attribute__((interrupt, aligned(64))));
void pwm2_isr(){
static uint32_t count = 0;
uint32_t plic_id;
count++;
// Мигающий код
if(count == 500)
Red_V_clear_pin(5); // Выключите бортовой светодиод
if(count == 1000){
Red_V_set_pin(5); // Включите бортовой светодиод
count = 0;
}
Red_V_PWM2_clear_flag(0); // Очистить флаг PWM2_0
plic_id = Red_V_PLIC_claim; // Уровень 2: заявить о прерывании GPIO
Red_V_PLIC_claim = plic_id;
}
// Пользовательская функция задержки
void delay(int number_of_microseconds){
clock_t start_time = clock() // Хранение времени старта
while (clock() < start_time + number_of_microseconds);
}
int main (void) {
unsigned int dc = 50; // Дежурный цикл по умолчанию 50%
struct metal_gpio *gpio_0; // Создаем экземпляр GPIO
struct metal_pwm *pwm_2; // Экземпляр PWM
struct metal_pwm *pwm_1; // Экземпляр ШИМ
gpio_0 = metal_gpio_get_device(0);
// Конфигурация бортового светодиода
metal_gpio_disable_input(gpio_0, 5);
Red_V_enable_output(5);
metal_gpio_disable_pinmux(gpio_0, 5);
metal_gpio_set_pin(gpio_0, 5, 1);
// Конфигурация кнопок
metal_gpio_enable_input(gpio_0, 0); // включить вход 0
metal_gpio_enable_input(gpio_0, 1); // включить вход 1
metal_gpio_disable_output(gpio_0, 0); // отключение вывода 0
metal_gpio_disable_output(gpio_0, 1); // отключение выхода 1
metal_gpio_disable_pinmux(gpio_0, 0); // отключите альтернативные функции для вывода 0
metal_gpio_disable_pinmux(gpio_0, 1); // отключение альтернативных функций для вывода 1
Red_V_enable_pullup(0); // включение подтяжки для вывода 0
Red_V_enable_pullup(1); // включение подтяжки для вывода 1
// Конфигурация ШИМ. Мы будем использовать PWM1_1, который использует GPIO0_19.
Red_V_enable_DS(19); // Высокий уровень сигнала на выходе
Red_V_enable_XOR(19); // Инвертируем выход для активно-высокого ШИМ
pwm_1 = metal_pwm_get_device(1); // Получение экземпляра ШИМ-устройства
while (pwm_1 == NULL); // Остановка. Что-то пошло не так
metal_pwm_enable(pwm_1); // Включить ШИМ1
metal_gpio_disable_pinmux(gpio_0, 20); // Отключить альтернативные функции для пина 20
metal_gpio_disable_pinmux(gpio_0, 21); // Отключить альтернативные функции для вывода 21
metal_gpio_disable_pinmux(gpio_0, 22); // Отключение альтернативных функций для вывода 22
// Установите частоту ШИМ1 на 1 кГц
metal_pwm_set_freq(pwm_1, 1, 1000); // Установите частоту ШИМ1_1 на 1 кГц для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE); // Устанавливаем рабочий цикл для PWM1_1
metal_pwm_trigger(pwm_1, 1, METAL_PWM_CONTINUOUS); // Запуск в непрерывном режиме
// Конфигурация ШИМ. Мы будем использовать PWM2 в качестве периодического прерывания.
pwm_2 = metal_pwm_get_device(2); // Получение экземпляра ШИМ-устройства
while (pwm_2 == NULL); // Остановка. Что-то пошло не так
metal_pwm_enable(pwm_2); // Включить PWM0
metal_gpio_disable_pinmux(gpio_0, 10); // Отключить альтернативные функции для пина 20
metal_gpio_disable_pinmux(gpio_0, 11); // Отключить альтернативные функции для вывода 21
metal_gpio_disable_pinmux(gpio_0, 12); // Отключение альтернативных функций для вывода 22
metal_gpio_disable_pinmux(gpio_0, 13); // Отключение альтернативных функций для вывода 22
// Установите частоту ШИМ2 на 1 кГц
metal_pwm_set_freq(pwm_2, 1, 1000); // Установите частоту ШИМ2_1 на 1 кГц для светодиода
metal_pwm_trigger(pwm_2, 1, METAL_PWM_CONTINUOUS); // Запуск в непрерывном режиме
Red_V_PWM2_sticky();
Red_V_PWM2_1_set_value(8000);
// Конфигурация прерываний
// Уровень 3: нет регистра ie для каналов ШИМ.
Red_V_PWM2_clear_flag(0); // Очистить флаг PWM2_0
// Уровень 2: Настройка PLIC для PWM2
Red_V_PLIC_PWM2_set_priority(0,7); // Канал 0, приоритет 7
// Уровень 2: PLIC (IE2, бит 16) для PWM2_0
Red_V_PLIC_clear_ie(); // Отключите все остальные прерывания
Red_V_PLIC_set_ie2(16); // Включить ШИМ2_0
// Уровень 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: Установка базового вектора mtvec
METAL_CPU_SET_CSR(mtvec,pwm2_isr);
// Уровень 1: включение прерываний с помощью MEIE в mie[11]
METAL_CPU_GET_CSR(mie,saved_config);
saved_config |= (0x1U< <11);
METAL_CPU_SET_CSR(mie,saved_config);
while(1){
// Код ШИМ кнопок и внешнего светодиода
if(Red_V_read_pin(0) == 0){ // Считывание входного пина 0
dc = (dc>0)? dc - 25 : dc; // шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программная задержка
while(Red_V_read_pin(0) == 0); // Ждем, пока кнопка будет отпущена
delay(50000); // Программная задержка
}
else if(Red_V_read_pin(1) == 0){ // Считывание входного пина 1
dc = (dc<100)? dc + 25 : dc; // Шаги по 25% для светодиода
metal_pwm_set_duty(pwm_1, 1, dc, METAL_PWM_PHASE_CORRECT_DISABLE);
delay(50000); // Программная задержка
while(Red_V_read_pin(1) == 0); // Подождите, пока кнопка будет отпущена
delay(50000); // Программная задержка
}
}
return 0; // Недоступный код
}
Поздравляю! Вы, должно быть, многому научились только в этой лекции.
Давайте вспомним, что мы рассмотрели в этой лекции:
Не стесняйтесь возвращаться к этому материалу, если в будущем вам понадобится подтянуть знания.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.