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

Моё решение

Мне было немного непросто понять, как обработчику прерывания вычислить время следующего прерывания, чтобы сирена продолжала звучать. В итоге я завёл пару переменных состояния: чтобы отслеживать, включён ли вывод динамика или выключен (можно было бы проверить и аппаратно), и чтобы отслеживать, на каком этапе цикла «вверх — вниз» находится сирена.

Все подробности есть в моём коде (src/main.rs).

#![no_main]
#![no_std]

use cortex_m::asm;
use cortex_m_rt::entry;
use critical_section_lock_mut::LockMut;
use embedded_hal::{delay::DelayNs, digital::OutputPin};
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};

use microbit::{
    hal::{
        gpio,
        pac::{self, interrupt},
        timer,
    },
    Board,
};

/// Базовая частота сирены в Гц.
const BASE_FREQ: u32 = 440;
/// Максимальный подъём частоты сирены в Гц.
const FREQ_RISE: u32 = 220;
/// Время одного полного цикла в мкс.
const RISE_TIME: u32 = 500_000;

/// Эти удобные типы упрощают жизнь.
type SpeakerPin = gpio::Pin<gpio::Output<gpio::PushPull>>;
type SirenTimer = timer::Timer<pac::TIMER0>;

/// Текущее состояние сирены. Обновляется обработчиком прерывания
/// во время работы.
struct Siren {
    /// Таймер, который использует сирена.
    timer: SirenTimer,
    /// Вывод динамика MB2. Должен принадлежать
    /// этой структуре, чтобы его мог использовать обработчик прерывания.
    speaker_pin: SpeakerPin,
    /// Находится ли сейчас вывод динамика в высоком уровне?
    pin_high: bool,
    /// Время в мкс с начала текущего цикла сирены.
    cur_time: u32,
}

impl Siren {
    /// Создать новую сирену с заданной периферией.
    fn new(speaker_pin: SpeakerPin, timer: SirenTimer) -> Self {
        Self {
            timer,
            speaker_pin,
            pin_high: false,
            cur_time: 0,
        }
    }

    /// Запустить сирену.
    fn start(&mut self) {
        self.speaker_pin.set_low().unwrap();
        self.pin_high = false;
        self.cur_time = 0;
        self.timer.enable_interrupt();
        // Интервал таймера задаётся в тиках.
        // Таймер [nrf52833_hal] жёстко настроен на 1 млн тиков в секунду.
        self.timer.start(1_000_000 / BASE_FREQ);
    }

    /// Остановить сирену.
    fn stop(&mut self) {
        self.timer.disable_interrupt();
    }

    /// Перевести сирену к следующему изменению состояния динамика.
    /// Обычно вызывается из обработчика прерывания таймера.
    fn step(&mut self) {
        // Переключаем вывод динамика.
        if self.pin_high {
            self.speaker_pin.set_low().unwrap();
            self.pin_high = false;
        } else {
            self.speaker_pin.set_high().unwrap();
            self.pin_high = true;
        }

        // Вычисляем следующий период. Математика здесь немного
        // особенная.

        // Сначала, если нужно, переходим к следующему циклу сирены.
        while self.cur_time >= 2 * RISE_TIME {
            self.cur_time -= 2 * RISE_TIME;
        }
        // Затем определяем, где мы находимся в текущем цикле сирены.
        let cycle_time = if self.cur_time < RISE_TIME {
            self.cur_time
        } else {
            2 * RISE_TIME - self.cur_time
        };
        // Наконец, вычисляем частоту и период.
        let frequency = BASE_FREQ + FREQ_RISE * cycle_time / RISE_TIME;
        let period = 1_000_000 / frequency;

        // Предсказываем время следующего прерывания.
        self.cur_time += period / 2;

        // Обязательно сбросьте текущее прерывание перед
        // запуском следующего, иначе может произойти
        // новое прерывание сразу же.
        self.timer.reset_event();
        self.timer.start(period / 2);
    }
}

/// Сирена. Доступна и из обработчика прерывания,
/// и из основной программы.
static SIREN: LockMut<Siren> = LockMut::new();

/// Прерывание таймера для сирены. Просто переводит сирену к следующему шагу.
#[interrupt]
fn TIMER0() {
    SIREN.with_lock(|siren| siren.step());
}

#[entry]
fn main() -> ! {
    rtt_init_print!();
    let board = Board::take().unwrap();
    // Удобно использовать вывод, преобразованный через `degrade()`:
    // так не придётся выяснять точный тип вывода динамика.
    // Вывод хранится глобально в `SIREN`, поэтому его
    // размер должен быть известен.
    //
    // Типобезопасность при этом теряется, но вряд ли
    // это будет важно после этого места.
    let speaker_pin = board
        .speaker_pin
        .into_push_pull_output(gpio::Level::Low)
        .degrade();
    let timer0 = timer::Timer::new(board.TIMER0);
    let mut timer1 = timer::Timer::new(board.TIMER1);

    // Настраиваем NVIC для обработки прерываний.
    unsafe { pac::NVIC::unmask(pac::Interrupt::TIMER0) };
    pac::NVIC::unpend(pac::Interrupt::TIMER0);

    // Размещаем структуру сирены там, где её найдёт обработчик прерывания.
    let siren = Siren::new(speaker_pin, timer0);
    SIREN.init(siren);

    // Запускаем сирену и отсчитываем время.
    SIREN.with_lock(|siren| siren.start());
    for t in (1..=10).rev() {
        rprintln!("{}", t);
        timer1.delay_ms(1_000);
    }
    rprintln!("launch!");
    SIREN.with_lock(|siren| siren.stop());

    loop {
        asm::wfi();
    }
}