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

Определяем положение джойстика по значениям АЦП Raspberry Pi Pico

В этой главе мы будем выводить значения АЦП джойстика каждый раз, когда они меняются. Мы считаем два аналоговых сигнала, vrx и vry, которые подключены к осям джойстика, а также выведем значения при нажатии кнопки джойстика.

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

Проект из шаблона

Начнём с создания нового проекта из шаблона.

cargo generate --git https://github.com/ImplFerris/pico2-template.git --tag v0.3.2

Когда появится запрос, задайте проекту имя, например joystick-adc, и выберите embassy в качестве HAL.

Если у вас есть отладочный зонд, можно включить defmt, и логирование будет простым. Если отладочного зонда нет, отключите defmt и используйте вывод через USB Serial, как в предыдущей главе. Здесь эти шаги повторять не будем, чтобы сосредоточиться на настройке джойстика и АЦП.

Дополнительные импорты

Добавьте следующие импорты в файл main.rs. Они нужны для доступа к АЦП, входу GPIO и обработке прерываний.

#![allow(unused)]
fn main() {
// Для АЦП
use embassy_rp::adc::{Adc, Channel, Config as AdcConfig};
use embassy_rp::gpio::{Input, Pull};

// Привязка прерываний
use embassy_rp::adc;
use embassy_rp::bind_interrupts;
}

Привязка прерывания АЦП

Далее привязываем прерывание FIFO АЦП.

#![allow(unused)]
fn main() {
bind_interrupts!(struct Irqs {
    ADC_IRQ_FIFO => adc::InterruptHandler;
});
}

Настройка АЦП и джойстика

Теперь инициализируем периферию АЦП и настроим выводы джойстика. Сигналы vrx и vry подключены к выводам GPIO, которые поддерживают АЦП. Кнопку джойстика мы настраиваем как обычный вход.

#![allow(unused)]
fn main() {
// Настройка АЦП
let mut adc = Adc::new(p.ADC, Irqs, AdcConfig::default());

let mut vrx_pin = Channel::new_pin(p.PIN_27, Pull::None);
let mut vry_pin = Channel::new_pin(p.PIN_26, Pull::None);
let button = Input::new(p.PIN_15, Pull::Up);
}

Хранение предыдущих значений

Перед входом в главный цикл сохраняем несколько переменных с предыдущими показаниями. Это позволяет замечать, что значение действительно изменилось, а не выводить значения непрерывно.

#![allow(unused)]
fn main() {
let mut prev_vrx: u16 = 0;
let mut prev_vry: u16 = 0;
let mut print_vals = true;
let mut prev_btn_state = false;
}

Мы храним предыдущие показания по осям X и Y, держим флаг, который управляет тем, когда нужно выводить значения, а также запоминаем предыдущее состояние кнопки, чтобы определить событие нажатия.

Главный цикл

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

#![allow(unused)]
fn main() {
loop {
    let Ok(vry) = adc.read(&mut vry_pin).await else {
        continue;
    };
    let Ok(vrx) = adc.read(&mut vrx_pin).await else {
        continue;
    };
}

Определение значимых изменений

Аналоговые джойстики не бывают идеально стабильными. Даже без прикосновения показания могут немного колебаться. Чтобы вывод оставался читаемым, мы реагируем только тогда, когда изменение достаточно большое.

#![allow(unused)]
fn main() {
    if vrx.abs_diff(prev_vrx) > 100 {
        prev_vrx = vrx;
        print_vals = true;
    }

    if vry.abs_diff(prev_vry) > 100 {
        prev_vry = vry;
        print_vals = true;
    }
}

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

Вывод значений АЦП

Если установлен флаг print_vals, мы выводим текущее положение джойстика и сбрасываем флаг.

#![allow(unused)]
fn main() {
    if print_vals {
        print_vals = false;

        info!("X: {} Y: {}", vrx, vry);
    }
}

Обработка кнопки

Затем считываем кнопку джойстика. Поскольку она использует подтягивающий резистор, нажатая кнопка читается как низкий уровень.

#![allow(unused)]
fn main() {
    let btn_state = button.is_low();
    if btn_state && !prev_btn_state {
        info!("Button Pressed");

        print_vals = true;
    }
    prev_btn_state = btn_state;
}

Когда кнопка нажата, мы помечаем значения АЦП для вывода. Так можно увидеть точное положение джойстика в момент нажатия.

В конце цикла добавим короткую задержку.

#![allow(unused)]
fn main() {
    Timer::after_millis(100).await;
}
}

Полный код

#![no_std]
#![no_main]

use defmt::info;
use embassy_executor::Spawner;
use embassy_rp as hal;
use embassy_rp::block::ImageDef;
use embassy_time::Timer;

// Для АЦП
use embassy_rp::adc::{Adc, Channel, Config as AdcConfig};
use embassy_rp::gpio::{Input, Pull};

// Привязка прерываний
use embassy_rp::adc;
use embassy_rp::bind_interrupts;

//Обработчик паники
use panic_probe as _;
// Логирование через defmt
use defmt_rtt as _;

/// Сообщаем Boot ROM о нашем приложении
#[unsafe(link_section = ".start_block")]
#[used]
pub static IMAGE_DEF: ImageDef = hal::block::ImageDef::secure_exe();

bind_interrupts!(struct Irqs {
    ADC_IRQ_FIFO => adc::InterruptHandler;
});

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_rp::init(Default::default());

    // Настройка АЦП
    let mut adc = Adc::new(p.ADC, Irqs, AdcConfig::default());

    let mut vrx_pin = Channel::new_pin(p.PIN_27, Pull::None);
    let mut vry_pin = Channel::new_pin(p.PIN_26, Pull::None);
    let button = Input::new(p.PIN_15, Pull::Up);

    let mut prev_vrx: u16 = 0;
    let mut prev_vry: u16 = 0;
    let mut print_vals = true;
    let mut prev_btn_state = false;

    loop {
        let Ok(vry) = adc.read(&mut vry_pin).await else {
            continue;
        };
        let Ok(vrx) = adc.read(&mut vrx_pin).await else {
            continue;
        };

        if vrx.abs_diff(prev_vrx) > 100 {
            prev_vrx = vrx;
            print_vals = true;
        }

        if vry.abs_diff(prev_vry) > 100 {
            prev_vry = vry;
            print_vals = true;
        }

        if print_vals {
            print_vals = false;

            info!("X: {} Y: {}", vrx, vry);
        }

        let btn_state = button.is_low();
        if btn_state && !prev_btn_state {
            info!("Button Pressed");

            print_vals = true;
        }
        prev_btn_state = btn_state;

        Timer::after_millis(100).await;
    }
}

// Метаданные программы для `picotool info`.
// Это не обязательно, но рекомендуется добавить эти минимальные записи.
#[unsafe(link_section = ".bi_entries")]
#[used]
pub static PICOTOOL_ENTRIES: [embassy_rp::binary_info::EntryAddr; 4] = [
    embassy_rp::binary_info::rp_program_name!(c"joystick-adc"),
    embassy_rp::binary_info::rp_program_description!(c"your program description"),
    embassy_rp::binary_info::rp_cargo_version!(),
    embassy_rp::binary_info::rp_program_build_attribute!(),
];

// Конец файла

Клонируйте существующий проект

Можно клонировать проект, который я создал, и перейти в папку joystick-adc.

git clone https://github.com/ImplFerris/pico2-embassy-projects
cd pico2-embassy-projects/joystick/joystick-adc