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

Отправка значений АЦП джойстика в USB Serial (RP-HAL)

В предыдущей главе мы использовали Embassy, чтобы читать значения АЦП джойстика и выводить их через логирование. В первой версии этой книги я работал с RP-HAL, когда изучал экосистему. Я не хочу выбрасывать этот материал, потому что он может пригодиться для понимания старого кода или других подходов.

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

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

Чтобы настроить проект, выполните:

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

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

Затем перейдите в папку проекта:

cd PROJECT_NAME
# Например, если вы назвали проект «joystick-usb»:
# cd joystick-usb

Дополнительные крейты

Добавьте в Cargo.toml эти дополнительные крейты вместе с уже существующими зависимостями.

#![allow(unused)]
fn main() {
usb-device = "0.3.2"
usbd-serial = "0.2.2"
heapless = "0.8.0"

embedded_hal_0_2 = { package = "embedded-hal", version = "0.2.5", features = [
  "unproven",
] }
}

Первые три должны быть уже знакомы: они настраивают связь через USB Serial, чтобы передавать данные между Pico и компьютером. heapless — вспомогательная библиотека для буферов.

embedded_hal_0_2 — новый крейт. В вашем Cargo.toml, возможно, уже есть embedded-hal версии «1.0.0», поэтому можно задаться вопросом, зачем нужна эта версия. Причина в том, что Embedded HAL 1.0.0 не содержит трейта АЦП для чтения значений, а RP-HAL использует трейт из версии 0.2. (Не удаляйте существующий embedded-hal 1.0.0, просто добавьте эту версию рядом с ним.)

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

#![allow(unused)]
fn main() {
/// Этот трейт — интерфейс АЦП, который настроен на чтение конкретного канала
/// в момент запроса (в отличие от непрерывной асинхронной выборки).
use embedded_hal_0_2::adc::OneShot;

// Для USB Serial
use usb_device::{class_prelude::*, prelude::*};
use usbd_serial::SerialPort;
use heapless::String;
}

USB Serial

Убедитесь, что вы выполнили раздел про USB Serial и добавили в проект шаблонный код из него.

#![allow(unused)]
fn main() {
    let usb_bus = UsbBusAllocator::new(hal::usb::UsbBus::new(
        pac.USB,
        pac.USB_DPRAM,
        clocks.usb_clock,
        true,
        &mut pac.RESETS,
    ));

    let mut serial = SerialPort::new(&usb_bus);

    let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd))
        .strings(&[StringDescriptors::default()
            .manufacturer("implRust")
            .product("Ferris")
            .serial_number("12345678")])
        .unwrap()
        .device_class(2) // 2 для CDC, см.: https://www.usb.org/defined-class-codes
        .build();

    let mut buff: String<64> = String::new();
}

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

Подключение джойстика такое же, как и раньше. GPIO 27 и GPIO 26 подключены к выводам VRX и VRY джойстика и настроены как входы АЦП. GPIO 15 используется для кнопки джойстика с внутренним подтягивающим резистором.

#![allow(unused)]
fn main() {
let mut adc = hal::Adc::new(pac.ADC, &mut pac.RESETS);

//Вывод VRX
let mut adc_pin_1 = hal::adc::AdcPin::new(pins.gpio27).unwrap();
// Вывод VRY
let mut adc_pin_0 = hal::adc::AdcPin::new(pins.gpio26).unwrap();

let mut btn = pins.gpio15.into_pull_up_input();
}

Вывод координат

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

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

Чтение значений АЦП:

Сначала считываем значения АЦП для vrx и vry. Если при чтении возникает ошибка, мы её игнорируем и продолжаем цикл:

#![allow(unused)]
fn main() {
let Ok(vry): Result<u16, _> = adc.read(&mut adc_pin_0) else {
    continue;
};
let Ok(vrx): Result<u16, _> = adc.read(&mut adc_pin_1) else {
    continue;
};
}

Проверка изменений относительно порога:

Далее проверяем, превышает ли абсолютная разница между текущим и предыдущим значением vrx или vry порог (например, 100). Если да, обновляем предыдущее значение и выставляем флаг print_vals в true:

#![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 равен true, сбрасываем его в false и выводим координаты X и Y через USB Serial:

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

    buff.clear();
    write!(buff, "X: {} Y: {}\r\n", vrx, vry).unwrap();
    let _ = serial.write(buff.as_bytes());
}
}

Определение нажатия кнопки по переходу состояния

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

Для этого мы сохраняем предыдущее состояние кнопки и сравниваем его с текущим перед выводом сообщения «Button pressed». Если кнопка сейчас в низком состоянии (нажата), а предыдущее состояние было высоким (не нажата), мы считаем это новым нажатием и выводим сообщение. Затем обновляем предыдущее состояние, что обеспечивает корректное определение будущих переходов.

#![allow(unused)]
fn main() {
let btn_state = btn.is_low().unwrap();
if btn_state && !prev_btn_state {
    let _ = serial.write("Button Pressed\r\n".as_bytes());
    print_vals = true;
}
prev_btn_state = btn_state;
}

Полный код

#![no_std]
#![no_main]

use core::fmt::Write;
use embedded_hal::{delay::DelayNs, digital::InputPin};
use embedded_hal_0_2::adc::OneShot;
use hal::block::ImageDef;
use heapless::String;
use panic_halt as _;
use rp235x_hal as hal;

use usb_device::{class_prelude::*, prelude::*};
use usbd_serial::SerialPort;

#[link_section = ".start_block"]
#[used]
pub static IMAGE_DEF: ImageDef = hal::block::ImageDef::secure_exe();

const XTAL_FREQ_HZ: u32 = 12_000_000u32;

#[hal::entry]
fn main() -> ! {
    let mut pac = hal::pac::Peripherals::take().unwrap();
    let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);

    let clocks = hal::clocks::init_clocks_and_plls(
        XTAL_FREQ_HZ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();
    let mut timer = hal::Timer::new_timer0(pac.TIMER0, &mut pac.RESETS, &clocks);

    let sio = hal::Sio::new(pac.SIO);
    let pins = hal::gpio::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );
    // let mut led = pins.gpio25.into_push_pull_output();

    let usb_bus = UsbBusAllocator::new(hal::usb::UsbBus::new(
        pac.USB,
        pac.USB_DPRAM,
        clocks.usb_clock,
        true,
        &mut pac.RESETS,
    ));

    let mut serial = SerialPort::new(&usb_bus);

    let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x27dd))
        .strings(&[StringDescriptors::default()
            .manufacturer("implRust")
            .product("Ferris")
            .serial_number("12345678")])
        .unwrap()
        .device_class(2) // 2 для CDC, см.: https://www.usb.org/defined-class-codes
        .build();

    let mut btn = pins.gpio15.into_pull_up_input();

    let mut adc = hal::Adc::new(pac.ADC, &mut pac.RESETS);

    //Вывод VRX
    let mut adc_pin_1 = hal::adc::AdcPin::new(pins.gpio27).unwrap();
    // Вывод VRY
    let mut adc_pin_0 = hal::adc::AdcPin::new(pins.gpio26).unwrap();

    let mut prev_vrx: u16 = 0;
    let mut prev_vry: u16 = 0;
    let mut prev_btn_state = false;
    let mut buff: String<64> = String::new();
    let mut print_vals = true;
    loop {
        let _ = usb_dev.poll(&mut [&mut serial]);

        let Ok(vry): Result<u16, _> = adc.read(&mut adc_pin_0) else {
            continue;
        };
        let Ok(vrx): Result<u16, _> = adc.read(&mut adc_pin_1) 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;
        }

        let btn_state = btn.is_low().unwrap();
        if btn_state && !prev_btn_state {
            let _ = serial.write("Button Pressed\r\n".as_bytes());
            print_vals = true;
        }
        prev_btn_state = btn_state;

        if print_vals {
            print_vals = false;

            buff.clear();
            write!(buff, "X: {} Y: {}\r\n", vrx, vry).unwrap();
            let _ = serial.write(buff.as_bytes());
        }

        timer.delay_ms(50);
    }
}

#[link_section = ".bi_entries"]
#[used]
pub static PICOTOOL_ENTRIES: [hal::binary_info::EntryAddr; 5] = [
    hal::binary_info::rp_cargo_bin_name!(),
    hal::binary_info::rp_cargo_version!(),
    hal::binary_info::rp_program_description!(c"JoyStick USB"),
    hal::binary_info::rp_cargo_homepage_url!(),
    hal::binary_info::rp_program_build_attribute!(),
];

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

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

git clone https://github.com/ImplFerris/pico2-rp-projects
cd pico2-rp-projects/joystick-usb/

Как запустить?

Прошивка (запуск кода) на Pico выполняется как обычно. Однако нужно настроить tio для взаимодействия с Pico через последовательный порт (/dev/ttyACM0). Это позволит читать данные с Pico и отправлять их на него.

tio

Убедитесь, что в вашей системе установлен tio. Если нет, его можно установить командой:

apt install tio

Подключение к последовательному порту

Выполните следующую команду, чтобы подключиться к последовательному порту Pico:

tio /dev/ttyACM0

Откроется сеанс терминала для общения с Pico.

Прошивка и запуск кода

Откройте другой терминал, перейдите в папку проекта и прошейте код на Pico как обычно:

cargo run

Если всё настроено правильно, в терминале tio вы увидите сообщение «Connected». Когда вы двигаете джойстик, будут выводиться координаты. Нажатие ручки вниз также покажет сообщение «Button pressed».