Главный цикл и прерывания

В отличие от предыдущего подхода, где код выполняется линейно, этот метод подразумевает асинхронное выполнение участков кода вне главного цикла. Другими словами, если вам во время чтения книги кто-то позвонил, вы непременно прервете чтение и ответите на звонок. По завершении разговора, положив трубку, вы откроете книгу там, где остановились, и продолжите читать. Мы рассматривали данную сущность, когда говорили про архитектуру МК, — она называется прерыванием (или Interrupt Service Routine, IST).

Так, если вы изначально не намерены отвечать на звонки во время чтения, то когда вы не взяли трубку, произошло событие, но не прерывание. В предыдущем методе мы как раз-таки реагировали на событие — состояние входного регистра порта (IDR) изменилось. Событием может быть что угодно, например, завершение отправки данных через SPI или переполнение счетчика таймера.

Сами прерывания бывают внутренними и внешними по отношению к ядру. В нашем случае нажатие кнопки — это внешнее прерывание. В STM32 за них отвечает модуль EXTI. Для обработки исключительного события (прерывания) необходимо задать имя функции, называемой обработчиком. Ее название фиксировано для каждого источника, и найти его можно в таблице векторов прерываний, указанных в startup-файле. В нашем случае пусть это будет EXTI_0_IRQHandler.

void EXTI0_IRQHandler(void) {
    // do something useful
    EXTI->PR |= EXTI_PR_PR0; // reset
}

Код, записанный в обработчике, необходимо выполнять как можно быстрее. Если главный цикл отвечает за стимуляцию мышц сердца, то слишком долгое «зависание» в прерывании лишит микроконтроллер возможности производить необходимые воздействия на мышцы, что делает нашу новую функцию скорее вредной, чем полезной. Оно и понятно, прерывания должны служить причиной изменения состояния устройства, а не выполнять работу основной программы.

Если наша программа делает что-то сложнее включения/выключения светодиода по нажатию кнопки, то на ум сразу приходит идея использовать какую-нибудь глобальную переменную, которую будет изменять прерывание, а использовать основная программа. Допустим, мы хотим задать длительность свечения светодиода:

volatile uint32_t ms = 1000; // ms
void EXTI0_IRQHandler(void) {
    delay = (delay > 10000) ? 1000 : (delay + 1000);
    EXTI->PR |= EXTI_PR_PR0; // reset
}

int main(void) {
    while (1) {
        led_on();
        delay(ms);
        led_off();
        delay(1000);
    }
}

Теперь при нажатии на кнопку длительность свечения будет увеличиваться на 1 секунду (до 10), затем сбросится до минимума (до одной секунды). Пример весьма искусственный — чаще приходится использовать некоторую переменную-флаг (принимающую одно из двух значений, 0 или 1).

volatile uint32_t motor_state = 0; // 0 == off, 1 == on

Для описания более сложного поведения приходится вводить всё больше и больше флагов, которые могут пересекаться друг с другом, усложняя и усложняя поддержку кода. Развитием идеи использования флагов для контроля работы устройства является машина состояний, которую мы рассмотрим чуть позже (тем более реализовать ее можно разными способами).

Как видите, такой подход дает большую гибкость (в сравнении с линейной программой), и его хватит для более сложной и большой прошивки, однако он тоже имеет свой предел применимости.


Изменено: