STM32F4 Встроенный Rust на уровне HAL: таймерное ультразвуковое измерение расстояния


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

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

Введение

В этом посте я буду конфигурировать и настраивать таймер stm32f4xx-hal и периферийные устройства GPIO с ультразвуковым датчиком для измерения расстояния до препятствия. Измерение расстояния будет непрерывно собираться и отправляться на терминал ПК по UART. Я буду использовать приложение UART Serial Communication из предыдущего сообщения. Кроме того, я не буду использовать прерывания, а пример будет настроен как симплексная система, передающая данные только в одном направлении (к ПК).

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

Для целей этой статьи в идеале я хотел бы использовать режим захвата ввода периферийного устройства таймера. Позже я обнаружил, что захват ввода пока не поддерживается для stm32f4xx-hal. В результате я прибег к другому подходу, который позволяет достичь того же самого, но считается менее эффективным. Можно по-прежнему использовать захват ввода на уровне PAC, но для целей этой заметки я решил остановиться на HAL.

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

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

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

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

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

В дополнение к вышесказанному, вам необходимо установить какой-либо терминал последовательной связи на вашем компьютере. Некоторые рекомендации включают:

Для Windows:

  • PuTTy
  • Teraterm

Для Mac и Linux:

  • minicom

Некоторые инструкции по установке для различных операционных систем можно найти в Discovery Book.

Установка оборудования

Материалы

  • Плата Nucleo-F401RE

  • Seeed Studio Grove Base Shield V2.0

  • Ультразвуковой датчик расстояния Seeed Studio Grove. В модуле используется ультразвуковой датчик NU40C16T/R-1.

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

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

Подключения

  • Контакт ультразвукового эхо-сигнала подключен к контакту PA8 (разъем D7 Grove).
  • Линия UART Tx, которая подключается к ПК через встроенный USB-мост, проходит через контакт PA2 микроконтроллера. Это жестко подключенный контакт, то есть вы не можете использовать другой для этой установки. Если вы не используете другую плату, отличную от Nucleo-F401RE, то для определения номера вывода необходимо обратиться к соответствующей документации (справочному руководству или техническому описанию).

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

Используемый ультразвуковой датчик представляет собой одноконтактный интерфейсный датчик. Один контакт, называемый эхо-контактом, работает в двунаправленном режиме. Эхо-контакт, сначала работающий как вход, должен быть задействован импульсом шириной не менее 10 мс. Это заставит датчик испустить серию ультразвуковых импульсов, для которых он измеряет задержку распространения. После этого контакт «эхо» переключается на выход, обеспечивая длительность импульса, пропорциональную расстоянию до препятствия.

Расстояние до препятствия рассчитывается как:

расстояние(cm)=ширина эхо-импульса(us)292text{расстояние} (см) = frac{text{ширина импульса эхо-сигнала} (us)}{29*2}

расстояние (см)=29∗2 ширина импульса эха (us)

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

  1. Установите выход PA8 на низкий уровень на 5 мс для получения чистого низкого импульса
  2. Установите на выходе PA8 высокий уровень (триггер) на 10 мс.
  3. Переключите PA8 на вход
  4. Продолжайте опрашивать вход PA8, пока он не станет высоким.
  5. Как только на входе PA8 появится высокий уровень, запустите счетчик/таймер
  6. Продолжайте опрашивать вход PA8, пока он не станет низким
  7. Получите измерение длительности импульса от счетчика/таймера
  8. Вычислить расстояние и отправить результат в последовательный канал UART
  9. Вернуться к 1

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

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

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

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

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

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

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

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

2️⃣ Продвигаем GPIO-структуры уровня PAC: В начале мне нужно настроить пин echo как вход и получить обработчик для этого пина, чтобы я мог им управлять. Мне также нужно получить обработчик для пина UART Tx. Оба пина являются частью GPIOA. Прежде чем я смогу получить какие-либо ручки, мне нужно продвинуть структуру pac-уровня GPIOA, чтобы иметь возможность создавать ручки для отдельных выводов. Для этого я использую метод split() следующим образом:

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

3️⃣ Получите хэндл для пина echo и настройте его на вход: Как было сказано ранее, пин echo подключен к пину PA8 (пин 8 порта A). Поэтому мне нужно создать хэндл для пина echo, в котором PA8 будет настроен на вход. Я назову этот хэндл echo и настрою его следующим образом:

let mut echo = gpioa.pa8;
Вход в полноэкранный режим Выход из полноэкранного режима

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

Для более подробного описания управления GPIO, пожалуйста, обратитесь к моему прошлому посту Мигание, управляемое кнопкой GPIO.

4️⃣ Получение ручки и настройка кнопки ввода: Встроенная пользовательская кнопка на Nucleo-F401RE подключена к пину PC13 (Pin 13 Port C), как было сказано ранее. По умолчанию пины настроены на вход, поэтому при создании хэндла для кнопки мы не вызываем никаких специальных методов.

let button = gpioc.pc13;
Вход в полноэкранный режим Выход из полноэкранного режима

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

Конфигурация периферийного устройства последовательной связи:

1️⃣ Настройте системные часы: Системные часы должны быть настроены, так как они необходимы для настройки периферии UART. Для настройки системных часов нам нужно сначала раскрутить структуру 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();
Вход в полноэкранный режим Выход из полноэкранного режима

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

Использование частоты, отличной от 8 МГц, для HSE на плате Nucleo-F401RE приведет к тому, что UART будет выводить ошибочные символы. Это значение должно быть скорректировано в соответствии с индивидуальными настройками платы.

2️⃣ Получите ручку и настройте контакт последовательной передачи (Tx): Поскольку кнопкой Tx является PA2, ранее я уже создал хэндл для gpioa, который мне нужно использовать. Однако теперь, когда мы не используем пин как обычный вход или выход GPIO, это означает, что пин должен быть подключен к другому периферийному устройству внутри микроконтроллера. Контакт можно сконфигурировать следующим образом с помощью метода into_alternate().

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

3️⃣ Настройте последовательный периферийный канал: Если посмотреть на распиновку платы Nucleo-F401RE, то вывод PA2 линии Tx подключается к периферийному устройству USART2 в микроконтроллере. Это означает, что нам необходимо сконфигурировать USART2 и каким-то образом передать его на ручку вывода, который мы хотим использовать. Это делается следующим образом:

    let mut tx = dp
        .USART2
        .tx(
            tx_pin,
            Config::default()
                .baudrate(115200.bps())
                .wordlength_8()
                .parity_none(),
            &clocks,
        )
        .unwrap();
Войти в полноэкранный режим Выход из полноэкранного режима

tx_pin и clocks — это дескрипторы, которые мы создали ранее. Config — это тип struct, который содержит информацию о конфигурации, необходимую для настройки периферийного устройства UART. Здесь я создаю экземпляр Config с признаком default сначала для настройки параметров по умолчанию. После этого я применяю методы baudrate, wordlength_8 и parity_none для настройки периферийного устройства UART на нужные мне параметры. Полный список методов Config можно найти здесь. Я настроил параметры UART, как показано на рисунке, на скорость 115200 бит/с в бодах с 8 битами данных и без четности, также обычно называемой 8N1. Наконец, поскольку метод tx возвращает результат, мы должны развернуть его с помощью метода unwrap.

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

Более подробно о настройке UART можно прочитать в блоге UART Serial Communication.

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

В алгоритме есть шаг, на котором я должен обеспечить импульсный триггер шириной 10 мс. Для этого мне нужно использовать метод задержки, чтобы держать пин echo на высоком уровне в течение этого времени. Кроме того, на другом этапе я должен использовать таймер для определения длительности импульса. Для этого мне нужно сконфигурировать два периферийных устройства следующим образом:

1️⃣ Настройте таймер на задержку и получение ручки: Я буду использовать TIM1 для обеспечения блокирующей задержки. Я назову хэндл delay и создам его следующим образом:

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

2️⃣ Настройте таймер для измерения импульса и получите хэндл: Я буду использовать TIM2 для обеспечения счетчика, который я могу использовать для получения Duration. Я назову хэндл counter и создам его следующим образом:

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

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

Более подробно о таймерах/счетчиках и их настройке можно прочитать в блоге Button Controlled Blinking by Timer Polling.

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

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

Следуя схеме, описанной ранее, мне сначала нужно установить выход пина echo в низкий уровень на 5 мс, чтобы получить чистый низкий импульс. Проблема заключается в том, что вывод echo сконфигурирован как вход. В результате, если рассмотреть общие методы Pin, существует метод with_push_pull_output_in_state, который, согласно его описанию, временно конфигурирует пин как push-pull выход и имеет следующую сигнатуру:

pub fn with_push_pull_output_in_state<R>(
    &mut self,
    state: PinState,
    f: impl FnOnce(&mut Pin<P, N, Output<PushPull>>) -> R
) -> R
Войти в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание, что метод имеет закрытие f, которое вызывается с переконфигурированным пином. После возврата закрытия пин будет сконфигурирован обратно в исходную конфигурацию. Кроме того, метод имеет параметр state, который позволяет мне назначить определенное состояние выходного пина (высокий или низкий уровень) при его реконфигурации. Таким образом, я могу добиться желаемого следующим образом:

echo.with_push_pull_output_in_state(PinState::Low, |_f| delay.delay_us(5_u32));
Войти в полноэкранный режим Выход из полноэкранного режима

Здесь происходит то, что вывод echo переконфигурируется в push pull output, при этом на выходе будет низкий уровень. В закрытии я ввожу задержку в 5 секунд с помощью ручки delay. Это означает, что вывод будет оставаться выходом в низком состоянии в течение 5 с, а затем снова станет входом.

Шаги 2 и 3 алгоритма требуют, чтобы я установил выход пина echo в высокий уровень (триггер) на 10 секунд, а затем переключил echo обратно на вход. Это можно сделать точно так же, как и предыдущий шаг, следующим образом:

echo.with_push_pull_output_in_state(PinState::High, |_f| delay.delay_us(10_u32));
Войти в полноэкранный режим Выйти из полноэкранного режима

Основные отличия здесь в том, что аргумент state имеет значение High, а закрытие имеет задержку 10us.

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

while !(echo.is_high()) {}
Войти в полноэкранный режим Выйти из полноэкранного режима

Используя цикл while и метод is_high Pin, код обходит эту же строку, пока вход echo pin не станет высоким.

После этого необходимо запустить таймер. Используя созданный ранее хэндл counter и метод start Counter, счетчик запускается следующим образом:

counter.start(1000.millis()).unwrap();
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь timeout Duration предоставляется в качестве аргумента, который представляет максимальную продолжительность работы счетчика. Метод start также возвращает Result, поэтому мне пришлось развернуть его. Я выбрал длительность 1000 миллисекунд, поскольку она соответствует самому большому расстоянию, которое можно измерить.

Теперь, когда таймер запущен, следующий шаг требует, чтобы я продолжал опрашивать вход пина echo, пока он не станет низким. Это делается точно так же, как и раньше, но вместо этого используется метод is_low следующим образом:

while !(echo.is_low()) {}
Вход в полноэкранный режим Выйти из полноэкранного режима

Как только пин echo становится низким, измерение длительности импульса должно быть собрано счетчиком/таймером следующим образом:

let duration = counter.now().duration_since_epoch();
counter.cancel().unwrap();
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь используется метод now для получения текущего Instance и метод duration_since_epoch для получения значения Duration. Я также отменяю/останавливаю таймер, используя метод cancel Counter и разворачиваю его.

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

Опять же, если не хватает ясности относительно методов счетчика, я бы рекомендовал обратиться к статье в блоге Button Controlled Blinking by Timer Polling, так как она более подробна.

Теперь, когда длительность импульса доступна, можно рассчитать расстояние. Используя представленную ранее формулу, расстояние в сантиметрах рассчитывается с помощью следующего кода:

let distance_cm = duration.to_micros() / 2 / 29;
Войти в полноэкранный режим Выйти из полноэкранного режима

Метод to_micros преобразует Duration в целое число микросекунд.

Наконец, результат отправляется по UART с помощью макроса writeln!:

writeln!(tx, "Distance {:02} cmr", distance_cm).unwrap();
Вход в полноэкранный режим Выход из полноэкранного режима

Если вы заметили, writeln! принимает три параметра, и в первом параметре writeln! я передаю последовательный обработчик tx в качестве аргумента. Кроме того, макрос writeln! необходимо развернуть, поскольку он возвращает Result. Третий параметр writeln! также содержит переменную distance_cm, которая была создана в предыдущей строке для хранения результата вычисления расстояния.

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

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

#![no_std]
#![no_main]

// Imports
use core::fmt::Write; // allows use to use the WriteLn! macro for easy printing
use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{
    gpio::PinState,
    pac::{self},
    prelude::*,
    serial::config::Config,
};

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

    // Configure the ultasonic device echo pin as input and obtain handler.
    let gpioa = dp.GPIOA.split();
    let mut echo = gpioa.pa8;

    // Serial config steps:
    // 1) Need to configure the system clocks
    // - Promote RCC structure to HAL to be able to configure clocks
    let rcc = dp.RCC.constrain();
    // - Configure system clocks
    // 8 MHz must be used for the Nucleo-F401RE board according to manual
    let clocks = rcc.cfgr.use_hse(8.MHz()).freeze();
    // 2) Configure/Define TX pin
    // Note that we already split port A earlier for the led pin
    // Use PA2 as it is connected to the host serial interface
    let tx_pin = gpioa.pa2.into_alternate();
    // 3) Configure Serial perihperal channel
    // We're going to use USART2 since its pins are the ones connected to the USART host interface
    // To configure/instantiate serial peripheral channel we have two options:
    // Use the device peripheral handle to directly access USART2 and instantiate a transmitter instance
    let mut tx = dp
        .USART2
        .tx(
            tx_pin,
            Config::default()
                .baudrate(115200.bps())
                .wordlength_8()
                .parity_none(),
            &clocks,
        )
        .unwrap();

    // Delay Configuration
    // Set up a microsecond delay handler
    let mut delay = dp.TIM1.delay_us(&clocks);

    // Counter/timer congig
    // Set up a microsecond counter handler
    let mut counter = dp.TIM2.counter_us(&clocks);

    // Algorithim
    // 1) Set pin ouput to low for 5 us to get clean low pulse
    // 2) Set pin output to high (trigger) for 10us
    // 3) Switch back to input
    // 4) Keep checking if pin goes high
    // 5) Once pin goes high start kick off counter/timer
    // 6) Wait for Pin to go low
    // 7) Obtain pulse measurement from timer
    // 8) Print out measurement on Serial
    // 9) Go back to 1)

    // Application Loop
    loop {
        // 1) Set pin ouput to low for 5 us to get clean low pulse
        echo.with_push_pull_output_in_state(PinState::Low, |_f| delay.delay_us(5_u32));

        // 2) Set pin output to high (trigger) for 10us
        // 3) Switch back to input
        echo.with_push_pull_output_in_state(PinState::High, |_f| delay.delay_us(10_u32));

        // 4) Wait until pin goes high
        while !(echo.is_high()) {}

        // 5) Kick off timer measurement with a max timeout Duration of 100ms?? defined by data sheet (longest distance that can be measured)
        counter.start(1000.millis()).unwrap();

        // 6) Wait until pin goes low.
        while !(echo.is_low()) {}

        // 7) Stop timer and collect elapsed time
        let duration = counter.now().duration_since_epoch();
        counter.cancel().unwrap();

        // 8) Calculate the distance in cms using formula in datasheet
        let distance_cm = duration.to_micros() / 2 / 29;

        // 8) Send calculated distance to serial interface
        writeln!(tx, "Distance {:02} cmr", distance_cm).unwrap();
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

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

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