STM32F4 Встроенный Rust на уровне HAL: ШИМ зуммер


Эта запись в блоге является четвертой из серии постов, в которых я исследую различные периферийные устройства микроконтроллера STM32F401RE, используя встроенный Rust на уровне HAL. Пожалуйста, имейте в виду, что некоторые понятия в новых постах могут зависеть от понятий в предыдущих постах.

Если вам понравилась эта статья, пожалуйста, не забудьте подписаться на рассылку новостей здесь, чтобы быть в курсе новых статей блога.

Введение

В этом посте я рассмотрю использование периферийного устройства ШИМ в stm32f4xx-hal. Я буду конфигурировать и настраивать периферийное устройство ШИМ для воспроизведения различных тонов на зуммере. Различные тона будут использоваться для генерации мелодии.

Предварительные знания

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

  • Базовые знания о кодировании на языке Rust.
  • Знакомство с основным шаблоном для создания встраиваемых приложений в Rust.

Настройка программного обеспечения

Весь код, представленный в этом посте, а также инструкции по настройке среды и инструментария доступны в git-репо apollolabsdev Nucleo-F401RE. Обратите внимание, что если код в git-репозитории немного отличается, это означает, что он был изменен для улучшения качества кода или обновления HAL/Rust.

Настройка аппаратного обеспечения

Материалы

  • Плата Nucleo-F401RE

  • Seeed Studio Grove Base Shield V2.0

  • Seeed Studio Grove — пьезо зуммер/активный зуммер.

Подключения

  • Положительная клемма зуммера подключена к контакту PA9 (через разъем D8 Grove Base Shield).
  • Отрицательная клемма зуммера подключена к GND (через разъем D8 Grove Base Shield).

🚨 Важное замечание:

Я использовал модульную систему Grove для удобства подключения. Это более элегантный подход и менее подвержен ошибкам. При необходимости можно напрямую подключить зуммер к контактам платы.

Дизайн программного обеспечения

📝 Примечание:

Код в этом посте является адаптацией в Rust примера зуммера, предоставленного Seeed Studio здесь.

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

После получения этой информации и конфигурирования устройства алгоритмические шаги выглядят следующим образом:

  1. Из структуры данных массива мелодий получить ноту и связанный с ней такт
  2. Из массива tones получить частоту, связанную с нотой, полученной на шаге 1.
  3. Проиграйте ноту в течение желаемой длительности (количество ударов * темп).
  4. Включите половину такта тишины (0 частот) между нотами.
  5. Вернитесь к шагу 1.

Между этими шагами есть тонкие детали, связанные с ШИМ, которые будут подробно рассмотрены в реализации.

Реализация кода

Импорт крейтов

В данной реализации необходимы следующие крейты:

  • Крейт cortex_m_rt для кода запуска и минимального времени выполнения для микроконтроллеров Cortex-M.
  • Крейт panic_halt для определения поведения паники для остановки при панике.
  • Крейт stm32f4xx_hal для импорта аппаратных абстракций устройств микроконтроллеров STMicro серии STM32F4 поверх API доступа к периферии.
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
    pac::{self},
    prelude::*,
    timer::Channel,
};
Вход в полноэкранный режим Выход из полноэкранного режима

Код конфигурации периферии

Конфигурация периферии GPIO:

1️⃣ Получение хэндла для периферийных устройств устройства: Во встроенном Rust, как часть паттерна проектирования singleton, мы сначала должны получить периферийные устройства уровня PAC. Это делается с помощью метода take(). Здесь я создаю обработчик периферийного устройства с именем dp следующим образом:

let dp = pac::Peripherals::take().unwrap();
Вход в полноэкранный режим Выход из полноэкранного режима

2️⃣ Продвигаем GPIO-структуры уровня PAC: В этом приложении мне понадобится один пин, который будет генерировать выходной сигнал ШИМ. Используемый пин — PA9, как было сказано ранее, и это один из пинов, поддерживающих вывод ШИМ. Фактически, он может быть внутренне подключен к таймеру 1 или TIM1. Прежде чем я смогу получить хэндл для PA9, мне нужно продвинуть структуру pac-уровня GPIOA, чтобы иметь возможность создавать хэндлы для отдельных пинов. Для этого я использую метод split() следующим образом:

let gpioa = dp.GPIOA.split();
Войти в полноэкранный режим Выход из полноэкранного режима

3️⃣ Получите ручку и настройте вывод ШИМ: Здесь PA9 нужно сконфигурировать так, чтобы он был подключен к внутренней схеме TIM1, которая позволяет генерировать выходной сигнал ШИМ. Это делается с помощью метода into_alternate общего типа Pin. Я назову ручку buzz и настрою ее следующим образом:

let buzz = gpioa.pa9.into_alternate();
Вход в полноэкранный режим Выход из полноэкранного режима

Периферийная конфигурация ШИМ-таймера:

1️⃣ Настройте системные часы: Системные часы должны быть настроены, так как они необходимы для настройки периферийного таймера. Для настройки системных часов нам нужно сначала раскрутить структуру RCC из PAC и ограничить ее с помощью метода constrain() (подробнее о методе constrain здесь), чтобы дать доступ к структуре cfgr. После этого мы создаем хэндл clocks, который предоставляет доступ к настроенным (и замороженным) системным часам. Часы настроены на использование частоты HSE 8 МГц путем применения метода use_hse() к структуре cfgr. Частота HSE определена в справочном руководстве к плате разработки Nucleo-F401RE. Наконец, метод freeze() применяется к структуре cfgr для замораживания конфигурации часов. Обратите внимание, что замораживание часов является механизмом защиты HAL, чтобы избежать изменения конфигурации часов во время выполнения. Из этого следует, что периферийные устройства, которым требуется информация о часах, примут только замороженную конфигурационную структуру Clocks.

let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
Вход в полноэкранный режим Выход из полноэкранного режима

2️⃣ Получение хэндла ШИМ и настройка таймера: Структура ШИМ в stm32f4xx-hal несколько запутана. В устройстве STM32F401 таймер TIM1 имеет 4 канала, каждый из которых может быть подключен к выходным контактам ШИМ. В прошлых постах при конфигурировании периферийных устройств, таких как UART, было показано, как периферийное устройство может быть сконфигурировано с помощью признаков расширения или абстракции периферии для создания экземпляра. Смущает то, что между чертами расширения и абстракциями, похоже, что различные абстракции pwm не имеют метода для инстанцирования экземпляра. Вместо этого, для инстанцирования периферийного устройства PWM (создания хэндла) есть следующие два варианта:

  1. Передать кортеж хэндлов Pin в один из признаков расширения pwm и применить метод split для получения кортежа хэндлов типа PWMChannel.
  2. Передайте один хэндл Pin в один из признаков расширения pwm, чтобы получить тип Pwm.

При использовании варианта 2, следовательно, потребуется передать номер канала в используемые методы, что будет показано позже. Проще говоря, в текущем состоянии stm32f4xx-hal экземпляры PWM могут быть созданы только путем применения трейтов к периферийным устройствам таймера. Так как я использую только один пин, мне нужен только один канал, поэтому я выбрал второй вариант для своего приложения. Я создал хэндл buzz_pwm следующим образом:

let mut buzz_pwm = dp.TIM1.pwm_hz(buzz, 2000.Hz(), &clocks);
Вход в полноэкранный режим Выход из полноэкранного режима

Разбирая эту строку, можно увидеть, что я использую метод pwm_hz, который имеет следующую сигнатуру:

    fn pwm_hz<P, PINS>(
        self, 
        pins: PINS, 
        freq: Hertz, 
        clocks: &Clocks
    ) -> PwmHz<Self, P, PINS>
    where
        PINS: Pins<Self, P>;
Войти в полноэкранный режим Выйти из полноэкранного режима

Как видно, метод pwm_hz возвращает тип PwmHz, в котором я также передаю в качестве аргументов хэндл пина buzz, частоту ШИМ и хэндл замороженных clocks. Причина, по которой я выбрал pwm_hz среди двух других доступных методов, заключается в том, что его методы позволят мне легче изменять выход ШИМ на основе значений частоты.

🚨 Важное замечание:

Во время написания этой заметки я заметил, что при использовании варианта 1, описанного ранее, возврат PWMChannel может оказаться довольно проблематичным. При навигации по документации, реализации PWMChannel не включают методы, позволяющие получить и установить период периферийного устройства. Для этого существует проблема, которую я представил здесь.

3️⃣ Настройте рабочий цикл ШИМ: Здесь все, что мне нужно, это генерировать регулярную квадратную волну, поэтому мне нужен рабочий цикл 50%. Это делается в два этапа следующим образом:

let max_duty = buzz_pwm.get_max_duty();
buzz_pwm.set_duty(Channel::C2, max_duty / 2);
Войти в полноэкранный режим Выход из полноэкранного режима

Анализируя приведенные выше строки, первая строка применяет метод get_max_duty на хэндле buzz_pwm, который возвращает значение u16, представляющее максимальный рабочий цикл. Во второй строке применяется метод set_duty, который принимает два параметра, перечисление канала, к которому подключен этот вывод, и значение рабочего цикла. Из технического описания устройства можно определить, что вывод PA9 подключен к каналу 2 таймера. В любом случае, если программист по ошибке вставит неправильный номер канала, компилятор выдаст ошибку.

🚨 Важное замечание:

Я обнаружил, что если пропустить этот шаг (не настроить рабочий цикл), то ШИМ-выход будет работать неправильно. Я предполагал, что код вернется к значению по умолчанию, но это оказалось не так.

Конфигурация периферийных устройств таймера и задержки:

В алгоритм необходимо ввести задержку для управления темпом. Поскольку я уже использую TIM1, мне нужно задействовать другой таймер, в качестве которого я буду использовать TIM2. Я создаю миллисекундную задержку delay следующим образом:

let mut delay = dp.TIM2.delay_ms(&clocks);
Вход в полноэкранный режим Выход из полноэкранного режима

На этом конфигурация закончена! Теперь перейдем к коду приложения.

Код приложения

Согласно описанию дизайна программы, для хранения информации о тонах и мелодиях необходимы два массива. Первый массив tones, содержит коллекцию кортежей, которые обеспечивают отображение буквы ноты и соответствующей ей частоты. Второй массив tune содержит коллекцию кортежей, представляющих ноту, которую необходимо сыграть, и количество ударов на ноту. Обратите внимание, что массив tune содержит пустую ноту ' ', которая представляет тишину и не имеет соответствующего отображения в массиве tones.

    let tones = [
        ('c', 261.Hz()),
        ('d', 294.Hz()),
        ('e', 329.Hz()),
        ('f', 349.Hz()),
        ('g', 392.Hz()),
        ('a', 440.Hz()),
        ('b', 493.Hz()),
    ];

    let tune = [
        ('c', 1),
        ('c', 1),
        ('g', 1),
        ('g', 1),
        ('a', 1),
        ('a', 1),
        ('g', 2),
        ('f', 1),
        ('f', 1),
        ('e', 1),
        ('e', 1),
        ('d', 1),
        ('d', 1),
        ('c', 2),
        (' ', 4),
    ];
Вход в полноэкранный режим Выход из полноэкранного режима

Далее, прежде чем перейти к алгоритмическому циклу, необходимо определить темп, который будет использоваться в ручке delay. Переменная tempo создается следующим образом:

let tempo = 300_u32;
Вход в полноэкранный режим Выйти из полноэкранного режима

Далее цикл приложения выглядит следующим образом:

    loop {
        // 1. Obtain a note in the tune
        for note in tune {
            // 2. Retrieve the freqeuncy and beat associated with the note
            for tone in tones {
                // 2.1 Find a note match in the tones array and update frequency and beat variables accordingly
                if tone.0 == note.0 {
                    // 3. Play the note for the desired duration (beats*tempo)
                    // 3.1 Adjust period of the PWM output to match the new frequency
                    buzz_pwm.set_period(tone.1);
                    // 3.2 Enable the channel to generate desired PWM
                    buzz_pwm.enable(Channel::C2);
                    // 3.3 Keep the output on for as long as required
                    delay.delay_ms(note.1 * tempo);
                } else if note.0 == ' ' {
                    // 2.2 if ' ' tone is found disable output for one beat
                    buzz_pwm.disable(Channel::C2);
                    delay.delay_ms(tempo);
                }
            }
            // 4. Silence for half a beat between notes
            // 4.1 Disable the PWM output (silence)
            buzz_pwm.disable(Channel::C2);
            // 4.2 Keep the output off for half a beat between notes
            delay.delay_ms(tempo / 2);
            // 5. Go back to 1.
        }
    }
Вход в полноэкранный режим Выйти из полноэкранного режима

Давайте разберем цикл построчно. Строка

for note in tune
Вход в полноэкранный режим Выход из полноэкранного режима

выполняет итерации по массиву tune, получая ноту с каждой итерацией. Внутри первого цикла вложен другой цикл for for tone in tones, который выполняет итерации по массиву tones. Во втором цикле из массива tune извлекается частота и ритм, связанные с каждой нотой. Оператор

if tone.0 == note.0
Войти в полноэкранный режим Выйти из полноэкранного режима

проверяет, есть ли соответствие между note и tone. Индекс .0 относится к первому индексу в кортеже, который является буквой ноты. Когда совпадение найдено, нота воспроизводится в течение требуемой длительности, которая равна количеству ударов, умноженному на темп. Это делается в три этапа:

Сначала с помощью метода set_period в абстракции PwmHz настраивается частота тона, чтобы соответствовать частоте найденного тона. Частота tone соответствует индексу 1 в кортеже и настраивается следующим образом:

buzz_pwm.set_period(tone.1);
Вход в полноэкранный режим Выход из полноэкранного режима

Во-вторых, с помощью метода enable включается канал buzz_pwm для активации желаемого ШИМ.

buzz_pwm.enable(Channel::C2);
Вход в полноэкранный режим Выход из полноэкранного режима

На третьем и последнем этапе выход поддерживается в течение периода такт*темп миллисекунд. Здесь я использую созданный ранее хэндл delay следующим образом:

delay.delay_ms(note.1 * tempo);
Войти в полноэкранный режим Выход из полноэкранного режима

В случае обнаружения ноты ' ' выполняются следующие строки:

else if note.0 == ' ' {
           buzz_pwm.disable(Channel::C2);
           delay.delay_ms(tempo);
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Что отключает выход канала ШИМ на один такт.

Наконец, после выхода из внутреннего цикла между нотами во внешнем цикле tune вводится полтакта тишины следующим образом:

buzz_pwm.disable(Channel::C2);
delay.delay_ms(tempo / 2);
Вход в полноэкранный режим Выход из полноэкранного режима

🚨 Важное замечание:

Некоторые из предыдущих методов не имеют описания в документации. Однако по названию самого метода можно понять, что он делает.

Полный код приложения

Здесь представлен полный код реализации, описанной в этом посте. Кроме того, вы можете найти полный проект и другие, доступные на git-репо apollolabsdev Nucleo-F401RE.

#![no_std]
#![no_main]

// Imports
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
    pac::{self},
    prelude::*,
    timer::Channel,
};

#[entry]
fn main() -> ! {
    // Setup handler for device peripherals
    let dp = pac::Peripherals::take().unwrap();

    // Set up the clocks
    let rcc = dp.RCC.constrain();
    let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();

    // Configure the Buzzer pin as an alternate and obtain handler.
    // I will use PA9 that connects to Grove shield connector D8
    // On the Nucleo FR401 PA9 connects to timer TIM1
    let gpioa = dp.GPIOA.split();
    let buzz = gpioa.pa9.into_alternate();
    let mut buzz_pwm = dp.TIM1.pwm_hz(buzz, 2000.Hz(), &clocks);

    // Configure the duty cycle to 50%
    // If duty not configured, PWM will not operate properly (suggest comments)
    let max_duty = buzz_pwm.get_max_duty();
    buzz_pwm.set_duty(Channel::C2, max_duty / 2);

    // Configure and create a handle for a second timer using TIM2 for delay puposes
    let mut delay = dp.TIM2.delay_ms(&clocks);

    // Define the notes and their frequencies
    let tones = [
        ('c', 261.Hz()),
        ('d', 294.Hz()),
        ('e', 329.Hz()),
        ('f', 349.Hz()),
        ('g', 392.Hz()),
        ('a', 440.Hz()),
        ('b', 493.Hz()),
    ];

    // Define the notes to be played and the beats per note
    let tune = [
        ('c', 1),
        ('c', 1),
        ('g', 1),
        ('g', 1),
        ('a', 1),
        ('a', 1),
        ('g', 2),
        ('f', 1),
        ('f', 1),
        ('e', 1),
        ('e', 1),
        ('d', 1),
        ('d', 1),
        ('c', 2),
        (' ', 4),
    ];

    // Define the tempo
    let tempo = 300_u32;

    // Application Loop
    loop {
        // 1. Obtain a note in the tune
        for note in tune {
            // 2. Retrieve the freqeuncy and beat associated with the note
            for tone in tones {
                // 2.1 Find a note match in the tones array and update frequency and beat variables accordingly
                if tone.0 == note.0 {
                    // 3. Play the note for the desired duration (beats*tempo)
                    // 3.1 Adjust period of the PWM output to match the new frequency
                    buzz_pwm.set_period(tone.1);
                    // 3.2 Enable the channel to generate desired PWM
                    buzz_pwm.enable(Channel::C2);
                    // 3.3 Keep the output on for as long as required
                    delay.delay_ms(note.1 * tempo);
                } else if note.0 == ' ' {
                    // 2.2 if ' ' tone is found disable output for one beat
                    buzz_pwm.disable(Channel::C2);
                    delay.delay_ms(tempo);
                }
            }
            // 4. Silence for half a beat between notes
            // 4.1 Disable the PWM output (silence)
            buzz_pwm.disable(Channel::C2);
            // 4.2 Keep the output off for half a beat between notes
            delay.delay_ms(tempo / 2);
            // 5. Go back to 1.
        }
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

В этом посте создано приложение зуммера, воспроизводящего мелодию, использующее периферийный ШИМ-выход таймера для микроконтроллера STM32F401RE на плате разработки Nucleo-F401RE. Весь код был создан на уровне HAL с использованием stm32f4xx Rust HAL. Есть вопросы? Поделитесь своими мыслями в комментариях ниже 👇. Если вы нашли это полезным, не забудьте подписаться на рассылку новостей здесь, чтобы быть в курсе новых статей блога.

Оцените статью
devanswers.ru
Добавить комментарий