Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Опрос, честно говоря, никуда не годится

Ах да, поворотники обычно мигают, верно? Как бы нам расширить программу, чтобы светодиод-указатель мигал при нажатии кнопки? Мигание светодиода мы уже знаем по программе Hello World: включаем светодиод, ждём некоторое время, потом выключаем. Но как сделать это в главном цикле и при этом проверять нажатия кнопок? Можно попробовать так:

#![allow(unused)]
fn main() {
    loop {
        if button_a.is_low().unwrap() {
            // Мигаем левой стрелкой
            display.show(&LEFT_ARROW);
            timer.delay_ms(500_u32);
            display.show(&BLANK);
            timer.delay_ms(500_u32);
        } else if button_b.is_low().unwrap() {
            // Мигаем правой стрелкой
            display.show(&RIGHT_ARROW);
            timer.delay_ms(500_u32);
            display.show(&BLANK);
            timer.delay_ms(500_u32);
        } else {
            display.show(&BLANK);
        }
        timer.delay_ms(10_u32);
    }
}

Видите проблему? Здесь мы пытаемся делать две вещи одновременно:

  1. Проверять нажатия кнопок
  2. Мигать светодиодом

Но процессор может делать только одно дело за раз. Если нажать кнопку во время паузы мигания, процессор не сможет отреагировать, пока пауза не закончится и цикл не начнётся заново. В итоге программа почти не реагирует (попробуйте сами и оцените, насколько медленно срабатывает кнопка).

Более «умная» программа понимала бы, что во время паузы мигания процессор на самом деле ничего не делает. Пока программа ждёт окончания паузы, она вполне может заниматься другими делами, а именно проверять нажатия кнопок.

Суперциклы

Термин суперцикл (superloop) во встраиваемых системах обозначает главный управляющий цикл, который последовательно выполняет множество действий. Это естественное развитие простого потока управления, которым мы пользовались до сих пор. Чтобы обрабатывать логику, которая может восприниматься как несколько одновременно происходящих дел, нужно немного поумнее структурировать программу, чтобы достаточно быстро реагировать на события.

В случае нашей программы-поворотника, где нужно мигать светодиодами при нажатии кнопки и быстро прекращать мигание, когда кнопку отпускают, можно создать «конечный автомат» (state machine), который описывает различные состояния программы. Для кнопок у нас три состояния:

  1. Ни одна кнопка не нажата
  2. Нажата кнопка A
  3. Нажата кнопка B

Для дисплея тоже есть три состояния:

  1. Ни один светодиод не горит
  2. Дисплей в активной фазе мигания (светодиоды горят)
  3. Дисплей в неактивной фазе мигания (светодиоды погашены и ждут, когда закончится период мигания, чтобы снова загореться)

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

  1. Ни одна кнопка не нажата
  2. Нажата кнопка A, и дисплей в активной фазе мигания (на дисплее показана стрелка влево)
  3. Нажата кнопка A, и дисплей в неактивной фазе мигания (на дисплее ничего не показано)
  4. Нажата кнопка B, и дисплей в активной фазе мигания (на дисплее показана стрелка вправо)
  5. Нажата кнопка B, и дисплей в неактивной фазе мигания (на дисплее ничего не показано)

Когда какая-либо кнопка впервые нажимается и мы переходим из состояния (1) в состояние (2) или (4), запускаем счётчик таймера, который отсчитывает время с момента нажатия. Когда таймер достигает порога (например, полсекунды), а кнопка всё ещё нажата, переходим, соответственно, в состояние (3) или (5) и заново инициализируем счётчик. Когда таймер снова достигает порога, возвращаемся в состояние (2) или (4) соответственно. Если в любой момент в состояниях (2), (3), (4) или (5) кнопка оказывается отпущенной, переходим обратно в состояние (1).

Главный поток управления суперцикла будет многократно опрашивать кнопки, сравнивать текущее значение счётчика таймера (если он есть) с порогом и менять состояния, если выполняется одно из перечисленных условий.

Мы реализовали этот суперцикл в качестве демонстрации (examples/blink-held.rs), но упростили конечный автомат: светодиод мигает только тогда, когда удерживается кнопка A.

#![no_main]
#![no_std]

use cortex_m_rt::entry;
use embedded_hal::delay::DelayNs;
use embedded_hal::digital::{InputPin, OutputPin};
use microbit::hal::timer::Timer;
use microbit::{hal::gpio, Board};
use panic_rtt_target as _;
use rtt_target::rtt_init_print;

const ON_TICKS: u16 = 25;
const OFF_TICKS: u16 = 75;

#[derive(Clone, Copy)]
enum Light {
    Lit(u16),
    Unlit(u16),
}

impl Light {
    fn flip(self) -> Self {
        match self {
            Light::Lit(_) => Light::Unlit(OFF_TICKS),
            Light::Unlit(_) => Light::Lit(ON_TICKS),
        }
    }

    fn tick_down(self) -> Self {
        match self {
            Light::Lit(ticks) => Light::Lit(ticks.max(1) - 1),
            Light::Unlit(ticks) => Light::Unlit(ticks.max(1) - 1),
        }
    }
}

#[derive(Clone, Copy)]
enum Indicator {
    Off,
    Blinking(Light),
}

#[entry]
fn main() -> ! {
    rtt_init_print!();
    let board = Board::take().unwrap();
    let mut timer = Timer::new(board.TIMER0);

    // Настраиваем кнопки
    let mut button_a = board.buttons.button_a;

    // Настраиваем светодиод (верхний левый, row1, col1)
    let mut row1 = board
        .display_pins
        .row1
        .into_push_pull_output(gpio::Level::Low);
    let _col1 = board
        .display_pins
        .col1
        .into_push_pull_output(gpio::Level::Low);

    let mut state = Indicator::Off;
    loop {
        let button_pressed = button_a.is_low().unwrap();
        match (button_pressed, state) {
            // Выключаем индикатор, когда кнопка не нажата.
            (false, _) => {
                row1.set_low().unwrap();
                state = Indicator::Off;
            }
            //
            (true, Indicator::Off) => {
                row1.set_high().unwrap();
                state = Indicator::Blinking(Light::Lit(ON_TICKS));
            }
            (true, Indicator::Blinking(light)) => match light {
                Light::Lit(0) | Light::Unlit(0) => {
                    let light = light.flip();
                    match light {
                        Light::Lit(_) => row1.set_high().unwrap(),
                        Light::Unlit(_) => row1.set_low().unwrap(),
                    }
                    state = Indicator::Blinking(light);
                }
                Light::Lit(_) | Light::Unlit(_) => {
                    state = Indicator::Blinking(light.tick_down());
                }
            },
        }
        timer.delay_ms(10_u32);
    }
}

Это всё ещё немного сложно. Задержки в 10 мс в цикле вполне достаточно, чтобы заметить изменения состояния кнопок.

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

Конкурентность

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

Хорошее введение в конкурентность во встраиваемом контексте есть здесь; перед тем как двигаться дальше, его стоит прочитать.

А пока давайте внимательнее посмотрим, что происходит, когда мы вызываем button_a.is_low() или display_pins.row1.set_high().