Введение
Содержание материала
Это учебный материал Ferrous Systems «Embedded Rust на Espressif». Он разделён на два практикума: вводный и продвинутый. Вводный знакомит с основами разработки встраиваемых систем и с тем, как заставить плату взаимодействовать с внешним миром: реагировать на команды и отправлять данные с датчиков.
Продвинутый практикум развивает эти темы: обработка прерываний, низкоуровневый доступ к периферии и написание собственных драйверов.
Вы можете присоединиться к сообществу esp-rs в Matrix и задавать там любые технические вопросы. Сообщество открыто для всех.
⚠️ Эта книга использует экосистему
std(esp-idf-hal,esp-idf-sys,esp-idf-svc). Её поддерживает сообщество, и эти крейты официально не поддерживаются Espressif.Espressif официально поддерживает подход
no_stdчерезesp-hal. Эквивалентный материал для официально поддерживаемого подхода см. в курсе поno_std.
Переводы
Эта книга переведена добровольцами. Если вы хотите, чтобы ваш перевод был указан здесь, откройте PR с его добавлением.
Плата
Rust ESP Board обязательна1 для работы с этой книгой: эмуляторы вроде QEMU не поддерживаются.
Дизайн и изображения платы, распиновка и схемы также доступны в этом репозитории.
Если вы записались на один из курсов, плату вам предоставит Espressif.
Мы в первую очередь работаем с платформой ESP32-C3 — микроконтроллером на архитектуре RISC-V с широкими возможностями для IoT: встроенными Wi-Fi и Bluetooth 5 (LE), а также большими объёмами RAM и flash-памяти для сложных приложений. Значительная часть курса применима и к Xtensa, другой архитектуре Espressif, в частности к ESP32-S3. Общие принципы низкоуровневого доступа тоже сохраняются, но работа с конкретным железом во многом отличается. При необходимости обращайтесь к техническим справочным руководствам (C3, S3) или к другим техническим документам.
Знание Rust
- Базовый Rust, как в The Rust Book, главы 1–6. Глава 4 «Владение» не обязательна для полного понимания.
- The Rust on ESP Book не обязательна, но очень рекомендуется: она поможет разобраться в экосистеме Rust на ESP и во многих концепциях, которые будут разбираться на курсе.
-
Вводную часть можно пройти и на ESP32-C3-DevKitC-02, но мы этого не рекомендуем. Курс заметно проще проходить на том же оборудовании. ↩
Подготовка
В этой главе описаны учебные материалы, необходимое оборудование и инструкции по установке.
Значки и форматирование
В книге мы используем значки, чтобы выделять разные виды информации:
- ✅ Призыв к действию.
- ⚠️ Предупреждения и детали, требующие особого внимания.
- 🔎 Сведения, которые углубляются в тему, но не обязательны для понимания дальнейшего материала.
- 💡 Подсказки, которые могут помочь при выполнении упражнений.
Пример заметки: заметки такого вида содержат полезную информацию.
Аннотации в коде
В некоторых файлах Rust встречаются якорные комментарии:
// ANCHOR: test
let foo = 1;
...
// ANCHOR_END: test
Якорные комментарии можно игнорировать: они нужны только для того, чтобы вставлять фрагменты кода в эту книгу. См. документацию mdBook.
Необходимое оборудование
- Rust ESP Board: доступна на Mouser и AliExpress. Полный список продавцов.
- Кабель USB-C, подходящий для подключения платы к компьютеру разработчика.
- Точка доступа Wi-Fi с выходом в Интернет.
Дополнительный отладчик или программатор не нужен.
Симуляция проектов
Некоторые проекты можно симулировать, хотя бы частично, с помощью Wokwi. Вот список проектов с поддержкой симуляции Wokwi:
intro/hello-worldintro/http-clientadvanced/i2c-sensor-readingadvanced/i2c-driveradvanced/button-interrupt
Перед запуском любого проекта для симуляции нужно установить расширение. Чтобы симулировать один проект:
- Нажмите F1, выберите
Wokwi: Select Config Fileи укажитеwokwi.tomlнужного проекта.- Отредактируйте соответствующий
wokwi.toml, чтобы симулировать упражнение или решение.
- Отредактируйте соответствующий
- Соберите проект в режиме
debug. - Снова нажмите F1 и выберите
Wokwi: Start Simulator.
Также можно отладить проект.
Проверка готовности к работе
⚠️ Если вы участвуете в курсе, который ведёт Ferrous Systems, настоятельно советуем подготовиться по инструкциям этой главы хотя бы за один рабочий день до начала, чтобы убедиться, что всё готово. Если возникнут проблемы или понадобится поддержка, свяжитесь с нами.
⚠️ Если вы используете ESP32-C3-DevKitC-02, некоторые выводы и адреса ведомых устройств будут отличаться: плата похожа, но не идентична. Это касается решений в advanced/i2c-sensor-reading/ и advanced/i2c-driver/, где выводы и адреса для ESP32-C3-DevKitC-02 закомментированы.
Сопутствующие материалы
Проверка оборудования
Подключите Espressif Rust Board к компьютеру. Убедитесь, что загорается маленький красный контрольный светодиод.
Плата также должна предоставлять UART-порт через USB:
Windows: USB Serial Device (COM-порт) в Диспетчере устройств в разделе «Порты (COM и LPT)».
Linux: USB-устройство в выводе lsusb.
У устройства будет VID (Vendor ID) 303a и PID (Product ID) 1001. Префикс 0x в выводе lsusb не указывается:
$ lsusb | grep USB
Bus 006 Device 035: ID 303a:1001 Espressif USB JTAG/serial debug unit
Другой способ увидеть, какой порт и какие права назначены устройству, — проверить каталог /dev/serial/by-id:
$ ls -l /dev/serial/by-id
lrwxrwxrwx 1 root root .... usb-Espressif_USB_JTAG_serial_debug_unit_60:55:F9:C0:27:18-if00 -> ../../ttyACM0
Если вы используете ESP32-C3-DevKitC-02, команда будет
$ ls /dev/ttyUSB*
macOS: Устройство отображается как часть дерева USB в system_profiler:
$ system_profiler SPUSBDataType | grep -A 11 "USB JTAG"
USB JTAG/serial debug unit:
Product ID: 0x1001
Vendor ID: 0x303a
(...)
Устройство также появится в каталоге /dev как tty.usbmodem:
$ ls /dev/tty.usbmodem*
/dev/tty.usbmodem0
Программное обеспечение
Следуйте шагам ниже для стандартной установки инструментов платформы ESP32-C3.
🔎 Если вам нужна нестандартная установка (например, сборка компонентов из исходников или добавление поддержки целевых платформ Xtensa), инструкции есть в главе Rust on ESP targets книги Rust on ESP.
Тулчейн Rust
✅ Если Rust ещё не установлен на вашем компьютере, получите его на https://rustup.rs/.
Кроме того, для ESP32-C3 сейчас нужна nightly-версия тулчейна Rust. Для этого курса мы будем использовать версию nightly-2024-06-30.
✅ Установите nightly-версию Rust и добавьте поддержку целевой архитектуры командой:
rustup toolchain install nightly-2024-06-30 --component rust-src
🔎 Rust умеет кросс-компилироваться под любую поддерживаемую цель (см. rustup target list). По умолчанию установлена только нативная архитектура вашей системы.
Для сборки под архитектуру Xtensa (она не входит в этот материал) по состоянию на январь 2022 года требуется форк компилятора Rust.
Инструменты Espressif
Понадобится несколько инструментов:
cargo-espflash— загрузка прошивки в микроконтроллер и открытие serial-монитора с интеграцией в Cargo;espflash— загрузка прошивки в микроконтроллер и открытие serial-монитора;ldproxy— зависимость тулчейна сборки Espressif.
✅ Установите их командой:
cargo install cargo-espflash espflash ldproxy
⚠️ Команды espflash и cargo-espflash, приведённые в книге, предполагают версию 2 или новее.
Зависимости тулчейна
Debian/Ubuntu
sudo apt install llvm-dev libclang-dev clang libudev-dev libuv1-dev pkgconf python3-venv python-is-python3
macOS
При использовании менеджера пакетов Homebrew, который мы рекомендуем:
brew install llvm libuv
Устранение неполадок
-
Python 3 — обязательная зависимость. Он предустановлен в стандартной macOS и обычно в популярных дистрибутивах Linux для настольных компьютеров. Известно, что установленный Python 2 с дополнением
virtualenv, которое на него указывает, может вызывать проблемы со сборкой. -
Ошибка
failed to run custom build command for libudev-sys vX.X.Xилиesp-idf-sys vX.X.X:На момент написания этого можно решить так:
- Выполните эту команду:
apt-get update \ && apt-get install -y vim nano git curl gcc ninja-build cmake libudev-dev python3 python3-pip libusb-1.0-0 libssl-dev \ pkg-config libtinfo5-
Перезапустите терминал.
-
Если не помогло, выполните
cargo clean, удалите папку~/.espressif(в Windows —%USERPROFILE%\.espressif) и пересоберите проект. -
В Ubuntu может понадобиться сменить ядро на
5.19. Версию ядра можно узнать командойuname -r.
Docker
Альтернативный вариант — использовать Docker. В репозитории есть Dockerfile с инструкциями по установке тулчейна Rust и всех необходимых пакетов. Эта виртуализированная среда предназначена для сборки бинарных файлов под целевую платформу Espressif. Прошивать бинарные файлы из контейнеров нельзя, поэтому есть два варианта:
- Выполнять команды прошивки, например
cargo-espflash, в основной системе. В этом случае рекомендуется держать открытыми два терминала:- в контейнере: собрать проект;
- в основной системе: использовать подкоманду
cargo-espflash, чтобы прошить программу в устройство.
- Использовать крейт
web-flash, чтобы прошить полученные из контейнера бинарные файлы. Контейнер уже содержитweb-flash. Вот как прошить результат сборки проектаhardware-check:web-flash --chip esp32c3 target/riscv32imc-esp-espidf/debug/hardware-check
✅ Установите Docker для вашей операционной системы.
✅ Получите образ Docker. Есть два способа:
- Собрать образ Docker из
Dockerfile:
Сборка образа занимает некоторое время (20–30 минут) в зависимости от ОС и оборудования.docker image build --tag rust-std-training --file .devcontainer/Dockerfile . - Скачать его с Docker Hub:
docker pull espressif/rust-std-training
✅ Запустите новый контейнер Docker:
- Для локального образа Docker:
docker run --mount type=bind,source="$(pwd)",target=/workspace,consistency=cached -it rust-std-training /bin/bash - С Docker Hub:
docker run --mount type=bind,source="$(pwd)",target=/workspace,consistency=cached -it espressif/rust-std-training:latest /bin/bash
Это запускает интерактивную оболочку в контейнере Docker. Также локальный репозиторий монтируется в папку /workspace внутри контейнера. Изменения в проекте в основной системе отражаются в контейнере и наоборот.
Дополнительное программное обеспечение
VS Code
Один из редакторов с хорошей поддержкой Rust — VS Code, он доступен для большинства платформ. При работе в VS Code мы рекомендуем следующие расширения, которые помогут в разработке:
Rust Analyzer— автодополнение и навигация по коду;Even Better TOML— редактирование конфигурационных файлов в формате TOML.
Есть и несколько полезных расширений для продвинутого использования:
VS Code и Dev Containers
Расширение для VS Code, которое может помочь в разработке внутри контейнера Docker, — Remote Containers.
Оно использует тот же Dockerfile, что и настройка Docker, но собирает образ и подключается к нему прямо из VS Code. После установки расширения VS Code распознаёт конфигурацию в папке .devcontainer. Чтобы подключить VS Code к контейнеру, используйте команду Remote Containers - Reopen in Container.
Репозиторий практикума
Весь материал находится в каталоге std-training этого репозитория.
✅ Клонируйте репозиторий и перейдите в папку практикума:
git clone https://github.com/r007/EmbeddedRust-Rus.git
cd EmbeddedRust-Rus/std-training
❗ Пользователи Windows могут столкнуться с проблемами из-за длинных путей.
Содержимое репозитория
advanced/— примеры кода и упражнения для продвинутого курса;book/— исходники этой книги в формате Markdown;common/— код, общий для обоих курсов;common/lib/— вспомогательные крейты;intro/— примеры кода и упражнения для вводного курса.
Несколько слов о конфигурации
Во всём практикуме мы используем toml-cfg как более удобную и безопасную альтернативу тому, чтобы хранить учётные данные и другую чувствительную информацию прямо в исходном коде. Вместо этого настройки хранятся в файле cfg.toml в корне соответствующего пакета.
Эта конфигурация содержит ровно один заголовок секции, совпадающий с именем вашего пакета (name = "your-package" в Cargo.toml). Конкретные настройки будут различаться в разных проектах:
[your-package]
user = "example"
password = "h4ckm3"
❗ Если вы копируете cfg.toml в новый проект, не забудьте изменить заголовок на [name-of-new-package].
Привет, плата!
Теперь всё готово для проверки согласованности!
✅ Подключите USB-C порт платы к компьютеру и перейдите в папку hardware-check репозитория практикума:
cd intro/hardware-check
Чтобы проверить подключение к Wi-Fi, нужно указать имя сети (SSID) и пароль (PSK). Эти данные хранятся в отдельном файле cfg.toml (он добавлен в .gitignore), чтобы их случайно не раскрыли при публикации исходного кода или создании pull request. Пример такого файла есть в репозитории.
✅ Скопируйте cfg.toml.example в cfg.toml (в той же папке) и отредактируйте его, указав свои данные:
⚠️ Диапазон 5 ГГц не поддерживается в ESP32-C3, поэтому используйте Wi-Fi с активным диапазоном 2,4 ГГц.
$ cp cfg.toml.example cfg.toml
$ $EDITOR cfg.toml
$ cat cfg.toml
[hardware-check]
wifi_ssid = "Your Wifi name"
wifi_psk = "Your Wifi password"
✅ Соберите, прошейте и откройте монитор проекта:
$ cargo run
Serial port: /dev/SERIAL_DEVICE
Connecting...
Chip type: ESP32-C3 (revision 3)
(...)
Compiling hardware-check v0.1.0
Finished release [optimized] target(s) in 1.78s
[00:00:45] ######################################## 418/418 segment 0x10000
Flashing has completed!
(...)
rst:0x1 (POWERON),boot:0xc (SPI_FAST_FLASH_BOOT)
(...)
(...)
(...)
I (4427) wifi::wifi: Wifi connected!
🔎 Если
cargo runвыполнился успешно, можно выйти сочетаниемctrl+C.
🔎
cargo runнастроен на использованиеespflashв качестве пользовательского раннера. Тот же результат можно получить так:
- с помощью
cargo-espflash:cargo espflash flash --release --monitor;- собрав проект и прошив его через
espflash:cargo build --release && espflash target/riscv32imc-esp-espidf/release/hardware-check. Для удобства эта настройка применена ко всем проектам курса.
При запуске светодиод платы должен загореться жёлтым. Затем, в зависимости от того, удалось ли подключиться к Wi-Fi, он либо загорится красным (ошибка), либо при успехе будет мигать, попеременно зелёным и синим. При ошибке Wi-Fi внизу также появится диагностическое сообщение, например:
Error: could not connect to Wi-Fi network: ESP_ERR_TIMEOUT
⚠️ Ошибка
ESP_ERR_TIMEOUTтакже возникнет, если имя сети или пароль указаны неверно, поэтому перепроверьте их.
Дополнительная информация о сборке, прошивке и мониторинге
Если хотите попробовать собрать проект без прошивки, выполните:
cargo build
Также можно открыть монитор устройства без прошивки командой:
espflash monitor
Устранение неполадок
Ошибки сборки
error[E0463]: can't find crate for `core`
= note: the `riscv32imc-esp-espidf` target may not be installed
Вы пытаетесь собрать проект на stable Rust, а нужен nightly.
Это сообщение об ошибке немного вводит в заблуждение: эту целевую платформу нельзя установить. Его нужно собрать из исходников с помощью build-std, а эта функция доступна только в nightly.
error: cannot find macro `llvm_asm` in this scope
Вы используете несовместимую версию nightly. Настройте подходящую через rust-toolchain.toml или cargo override.
CMake Error at .../Modules/CMakeDetermineSystem.cmake:129 (message):
Установка тулчейна Espressif могла повредиться. Удалите её и запустите сборку заново, чтобы инициировать новую загрузку:
rm -rf ~/.espressif
В Windows удалите папку %USERPROFILE%\.espressif.
Serial port: /dev/tty.usbserial-110
Connecting...
Unable to connect, retrying with extra delay...
Unable to connect, retrying with default delay...
Unable to connect, retrying with extra delay...
Error: espflash::connection_failed
× Error while connecting to device
╰─▶ Failed to connect to the device
help: Ensure that the device is connected and the reset and boot pins are not being held down
Плата недоступна через USB-C кабель. Типичная ошибка подключения выглядит так:
Обходные пути:
- Нажмите и удерживайте кнопку boot на плате, запустите команду прошивки и отпустите кнопку boot после начала прошивки.
- Используйте USB-хаб.
Вводный практикум
Этот практикум познакомит вас с основами разработки встраиваемых систем. К концу практикума мы научимся взаимодействовать с внешним миром, в том числе отправлять команды плате и получать данные с её датчиков. Программа включает:
- Обзор структуры проекта.
- Создание проекта с помощью
cargo-generate. - Написание HTTP-клиента.
- Написание HTTP-сервера.
- Написание MQTT-клиента, который:
- публикует данные датчиков;
- получает команды через подписку на топик.
Подготовка
Пройдите главу подготовки, чтобы подготовиться к этому практикуму.
Справочник
Если вы впервые занимаетесь встраиваемым программированием, прочитайте наш справочник, где в простой форме объясняются некоторые термины.
Организация проекта
Крейты esp-rs
В отличие от большинства других встраиваемых платформ, Espressif поддерживает стандартную библиотеку Rust. В частности, это значит, что в вашем распоряжении будут коллекции произвольного размера, например Vec или HashMap, а также универсальное хранение в куче через Box. Вы также можете свободно создавать новые потоки и использовать примитивы синхронизации, такие как Arc и Mutex, чтобы безопасно разделять данные между ними.
Тем не менее память — ограниченный ресурс во встраиваемых системах, поэтому нужно следить, чтобы её не хватило: особенно дорогими могут быть потоки.
Такие сервисы, как Wi-Fi, HTTP-клиент и сервер, MQTT, OTA-обновления, логирование и т. д., предоставляются через открытый фреймворк IoT от Espressif — ESP-IDF. Он в основном написан на C, поэтому в Rust его открывают по классической схеме разделения на крейты:
- крейт
sys, предоставляющий собственноunsafe-биндинги (esp-idf-sys); - крейт более высокого уровня с безопасными и удобными абстракциями на Rust (esp-idf-svc).
Последняя часть головоломки — низкоуровневый доступ к железу, который тоже предоставляется в разделённом виде:
- esp-idf-hal реализует не зависящие от железа трейты embedded-hal, такие как аналого-цифровое преобразование, цифровые выводы ввода-вывода или связь по SPI. Как следует из названия, он тоже использует ESP-IDF в качестве основы;
- если нужна прямая работа с регистрами, esp32c3 предоставляет крейт доступа к периферии, сгенерированный с помощью
svd2rust.
Подробнее — в главе об экосистеме книги The Rust on ESP Book.
Тулчейн сборки
🔎 В процессе сборки проекта esp-idf-sys загружает ESP-IDF — основанный на C набор инструментов Espressif. Каталог загрузки можно настроить. Чтобы сэкономить место на диске и время загрузки, все примеры и упражнения в репозитории практикума используют один общий (global) экземпляр этого набора, установленный в ~/.espressif (в Windows — %USERPROFILE%\.espressif). Другие варианты см. в описании параметра ESP_IDF_TOOLS_INSTALL_DIR в README esp-idf-sys.
Структура пакета
Помимо обычного содержимого проекта Rust, созданного командой cargo new, нужны несколько дополнительных файлов и параметров. Примеры и упражнения в этом практикуме уже полностью настроены, а для создания новых проектов рекомендуется пользоваться мастером на базе cargo-generate.
🔎 Остаток этой страницы — необязательные сведения, которые могут пригодиться, если вы захотите изменить некоторые аспекты проекта.
Нужно задать некоторые build-зависимости:
[build-dependencies]
embuild = "=0.31.2"
anyhow = "=1.0.71"
Дополнительные конфигурационные файлы
build.rs— скрипт сборки Cargo. Здесь задаются переменные окружения, необходимые для сборки..cargo/config.toml— задаёт целевую архитектуру, пользовательский раннер для прошивки и мониторинга устройства и управляет деталями сборки. Здесь же при необходимости переопределяетсяESP_IDF_TOOLS_INSTALL_DIR.sdkconfig.defaults— переопределяет специфичные для ESP-IDF параметры, такие как размер стека, уровень логирования и т. д.
Создание новых проектов
Теперь мы используем cargo-generate (универсальный мастер создания проектов), чтобы настроить наше первое приложение.
Подробнее о создании проектов — в главе Writing Your Own Application книги The Rust on ESP Book.
Большинство других упражнений этого практикума уже содержат готовую заготовку проекта и не требуют
cargo-generate.
✅ Установите cargo-generate:
cargo install cargo-generate
✅ Перейдите в папку intro и запустите cargo generate с шаблоном esp-idf:
cd intro
cargo generate esp-rs/esp-idf-template cargo
Мастер запросит данные о вашем новом проекте. Если предлагается выбор из нескольких вариантов, переключайтесь стрелками вверх и вниз и выбирайте клавишей Enter.
Первое сообщение, которое вы увидите:
⚠️Unable to load config file: /home/$USER/.cargo/cargo-generate.toml. Эта ошибка возникает, потому что у вас нет файла избранных настроек, но он вам не нужен, и это предупреждение можно игнорировать.
🔎 Вы можете создать файл избранных настроек, который будет размещён в $CARGO_HOME/cargo-generate, и переопределить его через -c, --config <config-file>.
Если ошиблись, нажмите
Ctrl+Cи начните заново.
✅ Настройте проект:
(Эти пункты могут появиться в другом порядке)
- Project Name:
hello-world - MCU:
esp32c3 - Configure advanced template options?:
false
🔎 .cargo/config.toml содержит локальные настройки (список всех настроек) для вашего пакета.
Cargo.toml содержит зависимости, а Cargo.lock фиксирует все ваши зависимости.
Необязательно, но рекомендуется: чтобы сэкономить место на диске и время загрузки, задайте общий каталог инструментов. Иначе каждый новый проект или рабочее пространство получит собственную копию этих инструментов на вашем компьютере:
✅ Откройте hello-world/.cargo/config.toml и добавьте следующую строку в конец секции [env]. Остальное не меняйте.
[env]
# ...
ESP_IDF_TOOLS_INSTALL_DIR = { value = "global" } # добавьте эту строку
✅ Откройте hello-world/rust-toolchain.toml и приведите файл к такому виду:
[toolchain]
channel = "nightly-2025-01-01" # измените эту строку
✅ Запустите проект следующей командой, находясь в папке hello-world:
cd hello-world
cargo run
✅ Последние строки вывода должны выглядеть так:
(...)
I (268) cpu_start: Starting scheduler.
Hello, world!
Дополнительные задания
- Если функция
mainзавершается, для повторного запуска нужно перезагрузить микроконтроллер. Что произойдёт, если вместо этого в конце поставить бесконечный цикл? Проверьте свою догадку, прошив программу с циклом. - Можете придумать, как избежать того, что вы сейчас видите? (нажмите, чтобы увидеть подсказку:1)
Устранение неполадок
- Если
cargo runзависает наConnecting..., возможно, ещё работает другой процесс монитора (например, оставшийся после начальной проверкиhardware-check). Попробуйте найти его и завершить. Если не помогло, отключите и снова подключите USB-кабель платы. ⛔ Git Error: authentication required: скорее всего, ваша конфигурация git подменяет HTTPS-адреса GitHub на SSH. Проверьте глобальный~/.git/configна наличие секцийinsteadOfи отключите их.
-
отдайте управление операционной системе, вызывая
sleepв цикле вместо активного ожидания (busy waiting). (используйтеuse std::thread::sleep) ↩
HTTP и HTTPS-клиент
В этом упражнении мы напишем небольшой клиент, который получает данные по HTTP-соединению из Интернета. Затем мы превратим его в HTTPS-клиент.
HTTP-клиент
Цель этого упражнения — написать небольшой HTTP-клиент, который подключается к веб-сайту.
Подготовка
✅ Перейдите в папку intro/http-client.
✅ Откройте подготовленную заготовку проекта в intro/http-client.
✅ Добавьте свои данные для подключения к сети в cfg.toml, как при проверке оборудования.
✅ Откройте документацию этого проекта командой:
cargo doc --open
intro/http-client/examples/http_client.rs содержит решение. Запустить его можно командой:
cargo run --example http_client
Установка соединения
По умолчанию доступен только незашифрованный HTTP, что довольно сильно ограничивает выбор хостов для подключения. Мы будем использовать http://neverssl.com/.
В ESP-IDF HTTP-соединениями клиента управляет http::client::EspHttpClient из крейта esp-idf-svc. Он реализует трейт http::client::Client из embedded-svc, который определяет функции для методов HTTP-запросов, таких как GET или POST. Самое время посмотреть документацию, которую вы открыли командой cargo doc --open, для esp_idf_svc::http::client::EspHttpConnection и embedded_svc::http::client::Client. Изучите доступные методы создания экземпляров.
✅ Создайте новый EspHttpConnection с конфигурацией по умолчанию. Подходящий конструктор найдите в документации.
✅ Получите клиент из только что созданного соединения.
Вызов HTTP-методов клиента (например, get(url)) возвращает embedded_svc::http::client::Request. Его нужно отправить явно: клиент может передавать данные вместе с запросом.
Функция get использует as_ref(). Это значит, что вместо ограничения одним конкретным типом, например только String или только &str, функция принимает любой тип, реализующий трейт AsRef<str>, то есть любой тип, для которого вызов .as_ref() даёт &str. Это работает для String и &str, а также для перечисления Cow<str>, которое содержит один из двух предыдущих вариантов.
#![allow(unused)]
fn main() {
let request = client.request(Method::Get, url.as_ref(), &headers)?;
}
Успешный ответ содержит код состояния в диапазоне 2xx, за которым следует исходный HTML сайта.
✅ Проверьте, что соединение прошло успешно.
✅ Возвращайте ошибку, если код состояния не входит в диапазон 2xx.
#![allow(unused)]
fn main() {
match status {
200..=299 => {
}
_ => bail!("Unexpected response code: {}", status),
}
}
Ошибку статуса можно вернуть с помощью крейта Anyhow. Он содержит различные средства для упрощения обработки ошибок на уровне приложения. Крейт предоставляет универсальный anyhow::Result<T>, который оборачивает успешный случай (Ok) в T и избавляет от необходимости указывать тип Err, при условии что каждая возвращаемая ошибка реализует std::error::Error.
✅ Читайте полученные данные по частям в буфер байтов (u8) с помощью Read::read(&mut reader,&mut buf). Read::read возвращает количество прочитанных байт. Чтение завершено, когда это значение равно 0.
✅ Выведите общее количество прочитанных байт.
✅ Выведите полученные данные в консоль.
💡 Ответ в буфере представлен байтами, поэтому может понадобиться метод для преобразования байтов в &str.
Дополнительные задания
✅ Обработайте коды 3xx, 4xx и 5xx, каждый в отдельной ветке match.
✅ Напишите собственное перечисление Error для представления этих ошибок. Реализуйте для него трейт std::error::Error.
Устранение неполадок
missing WiFi name/password: убедитесь, чтоcfg.tomlнастроен по образцуcfg.toml.example. Частая проблема — имя пакета и имя секции конфигурации не совпадают.
# Cargo.toml
#...
[package]
name = "http-client"
#...
# cfg.toml
[http-client]
wifi_ssid = "..."
wifi_psk = "..."
Guru Meditation Error: Core 0 panic'ed (Load access fault). Exception was unhandled.Это может быть вызвано.unwrap()в вашем коде. Попробуйте заменить их на оператор?.
HTTPS-клиент
Теперь внесите изменения в файлы HTTP-клиента, чтобы он работал и с зашифрованными соединениями.
intro/http-client/examples/https_client.rs содержит решение. Запустить его можно командой:
cargo run --example https_client
Создайте собственную конфигурацию клиента для esp_idf_svc::http::client::EspHttpConnection, которая включает использование этих сертификатов и задаёт значения по умолчанию для всего остального:
#![allow(unused)]
fn main() {
let connection = EspHttpConnection::new(&Configuration {
use_global_ca_store: true,
crt_bundle_attach: Some(esp_idf_svc::sys::esp_crt_bundle_attach),
..Default::default()
})?;
}
✅ Инициализируйте HTTP-клиент с новой конфигурацией и проверьте работу HTTPS, загрузив ресурс по https, например https://espressif.com/. Загрузка выведется в терминал как исходный HTML.
Устранение неполадок (повтор из предыдущего раздела)
missing WiFi name/password: убедитесь, чтоcfg.tomlнастроен по образцуcfg.toml.example. Частая проблема — имя пакета и имя секции конфигурации не совпадают.
# Cargo.toml
#...
[package]
name = "http-client"
#...
# cfg.toml
[http-client]
wifi_ssid = "..."
wifi_psk = "..."
Простой HTTP-сервер
Теперь мы превращаем плату в крошечный веб-сервер, который при получении запроса GET отдаёт данные с внутреннего датчика температуры.
Подготовка
Подготовленная заготовка проекта находится в intro/http-server/. Она уже включает установку подключения к Wi-Fi, но вам нужно настроить её на данные вашей сети в cfg.toml.
intro/http-server/examples/http_server.rs содержит решение. Запустить его можно командой:
cargo run --example http_server
Обработка запросов
Чтобы подключиться к плате из браузера, нужно знать её IP-адрес.
✅ Запустите код заготовки в intro/http-server. В выводе должен появиться IP-адрес платы, примерно такой:
I (3862) esp_netif_handlers: sta ip: 192.168.178.54, mask: ...
...
Server awaiting connection
sta ip — это «station», термин Wi-Fi для интерфейса, подключённого к точке доступа. Это адрес, который вы введёте в браузере (или в другом HTTP-клиенте, например curl).
🔎 ESP-IDF пытается зарегистрировать имя хоста
espressifв вашей локальной сети, поэтому часто работает иhttp://espressif/вместоhttp://<sta ip>/.Имя хоста можно изменить, задав
CONFIG_LWIP_LOCAL_HOSTNAMEвsdkconfig.defaults, например:CONFIG_LWIP_LOCAL_HOSTNAME="esp32c3"
Отправка HTTP-данных клиенту включает:
- создание экземпляра
EspHttpServer; - цикл в функции
main, чтобы она не завершалась: иначе сервер вышел бы из области видимости и остановился; - задание отдельной функции-обработчика
handlerдля каждого пути, по которому нужно отдавать содержимое. Любой не настроенный путь вернёт ошибку404. Эти обработчики реализуются как замыкания Rust прямо в коде:
#![allow(unused)]
fn main() {
server.fn_handler(path, Method::Get, |request| {
// ...
// создаём ответ
let mut response = request.into_ok_response()?;
// теперь можно записать нужные данные
response.write_all(&some_buf)?;
// когда закончите, обработчик ожидает в качестве результата `Completion`,
// это делается так:
Ok(())
});
}
✅ Создайте экземпляр EspHttpServer с конфигурацией по умолчанию esp_idf_svc::http::server::Configuration. Конфигурация по умолчанию автоматически заставит его слушать порт 80.
✅ Проверьте, что подключение к http://<sta ip>/ возвращает ошибку 404 (not found) с текстом This URI does not exist.
✅ Напишите обработчик запросов к корневому пути ("/"). Обработчик отправляет приветственное сообщение по адресу http://<sta ip>/, используя готовую функцию index_html() для генерации HTML-строки.
Динамические данные
Мы также можем передавать клиенту динамическую информацию. Заготовка включает настроенный temp_sensor, который измеряет внутреннюю температуру платы.
✅ Напишите второй обработчик, который отдаёт температуру чипа по адресу http://<sta ip>/temperature, используя готовую функцию temperature(val: f32) для генерации HTML-строки.
💡 Чтобы отправить строку ответа, её нужно преобразовать в срез &[u8] через a_string.as_bytes().
💡 Датчику температуры нужен эксклюзивный (изменяемый) доступ. Передать его в обработчик по значению не получится: он будет уничтожен после первого вызова. Это решается так: сделайте обработчик замыканием move || и оберните датчик в Arc<Mutex<_>>; один clone() этого Arc останется в функции main, а другой переместите в замыкание.
Устранение неполадок
httpd_txrx: httpd_resp_send_errможно исправить перезапуском, а если ничего не помогает, — командойcargo clean.- Убедитесь, что ваш компьютер и Rust ESP Board подключены к одной и той же сети Wi-Fi.
IoT на основе MQTT
В этом упражнении мы узнаем, как работает MQTT, и напишем приложение, которое может отправлять и получать данные через Интернет по MQTT.
Как работает MQTT
⚠️ Для этого упражнения нужен сервер MQTT. Если вы участвуете в курсе Ferrous Systems, учётные данные для сервера, которым управляет Espressif, будут выданы на практикуме. Иначе можно использовать один из серверов, перечисленных на https://test.mosquitto.org/, или установить сервер локально.
Чтобы завершить вводный практикум, добавим на плату немного функциональности IoT. Наша цель — заставить плату отправлять обновления показаний датчиков в реальном времени, без постоянного опроса, как пришлось бы делать с HTTP-сервером, а также получать команды для изменения цвета светодиода платы.
Это можно смоделировать с помощью архитектуры «издатель–подписчик»: несколько клиентов публикуют сообщения в определённых каналах (топиках) и могут подписываться на эти топики, чтобы получать сообщения от других. Доставкой сообщений управляет брокер — в нашем случае это сервер MQTT.
Сообщения MQTT
Сообщение MQTT состоит из двух частей: топика и полезной нагрузки (payload).
Топик выполняет ту же роль, что и тема письма или ярлык на картотечном шкафу, а полезная нагрузка содержит собственно данные. Формат данных полезной нагрузки не определён, хотя JSON встречается часто.
🔎 Последняя версия стандарта MQTT (MQTT 5) поддерживает метаданные о типе содержимого. При отправке сообщения MQTT нужно задать параметр качества обслуживания (QoS), определяющий гарантии доставки:
- не более одного раза;
- не менее одного раза;
- ровно один раз.
Для этого упражнения не важно, какой уровень качества вы выберете.
Топики MQTT
Топики MQTT — это строки в кодировке UTF-8, которые представляют иерархию, где уровни разделяются символом /. Ведущий слеш поддерживается, но не рекомендуется. Примеры топиков:
home/garage/temperature
beacons/bicycle/position
home/alarm/enable
home/front door/lock
Здесь датчик периодически публикует температуру в гараже, и она рассылается каждому подписчику, так же как маячок велосипеда публикует свои GPS-координаты. Топики alarm и lock служат приёмником команд для конкретных устройств. Однако ничто не мешает дополнительным подписчикам слушать эти команды, что может быть полезно для аудита.
🔎 Топики, которые начинаются с $, зарезервированы для внутренней статистики брокера. Обычно такой топик начинается с $SYS. Клиенты не могут публиковать сообщения в эти топики.
⚠️ Поскольку все участники практикума будут использовать один и тот же сервер MQTT, нужны меры, чтобы разные проекты не мешали друг другу. Заготовка упражнения генерирует уникальный случайный ID (в формате UUID v4) для каждой копии репозитория. Также можно сгенерировать свой UUID онлайн. Ваш UUID должен стоять в начале топиков сообщений, которыми обмениваются компьютер и плата, примерно по такому шаблону:
6188eec9-6d3a-4eac-996f-ac4ab13f312d/sensor_data/temperature
6188eec9-6d3a-4eac-996f-ac4ab13f312d/command/board_led
Подписка на топики
Клиент отправляет сообщения подписки, чтобы показать, что хочет получать определённые топики. Поддерживаются необязательные шаблоны (wildcards) — как для одного уровня иерархии, так и «для всего»:
home/garage/temperature— подписка только на этот конкретный топик;home/#— символ решётки используется как многоуровневый шаблон и подписывает на все топики, которые начинаются сhome/. Под него попадаютhome/garage/temperature,home/front door/lockиhome/alarm/enable, ноbeacons/bicycle/position— нет. Многоуровневый шаблон должен стоять в конце строки подписки;home/+/temperature— символ плюс служит одноуровневым шаблоном для подписки наhome/garage/temperature,home/cellar/temperatureи т. д.
Упражнение MQTT: отправка сообщений
Подготовка
✅ Перейдите в папку intro/mqtt/exercise.
✅ Откройте подготовленную заготовку проекта в intro/mqtt/exercise.
✅ В intro/mqtt/host_client находится программа для запуска на компьютере, которая имитирует поведение второго клиента. Запустите её в отдельном терминале командой cargo run. Подробнее о клиенте-хосте — ниже.
Клиент также генерирует случайные RGB-цвета и публикует их в топике. Это важно только для второй части упражнения.
⚠️ Как и в упражнениях с HTTP, нужно задать данные для подключения в cfg.toml обеих программ. Помимо данных Wi-Fi, нужно добавить параметры сервера MQTT. Нужные настройки смотрите в каждом cfg.toml.example. Напомним: имя в квадратных скобках в cfg.toml — это имя пакета из Cargo.toml.
Структура упражнения показана ниже. В этой части мы сосредоточимся на топике температуры.
intro/mqtt/exercise/solution/solution_publ.rs содержит решение. Запустить его можно командой:
cargo run --example solution_publ
Задания
✅ Создайте EspMqttClient с конфигурацией по умолчанию и пустым замыканием-обработчиком.
✅ Отправьте пустое сообщение в брокер в топик hello_topic. Для формирования правильного топика используйте служебную функцию hello_topic(uuid).
✅ Проверьте, что публикация прошла успешно: подключите клиент, который логирует эти сообщения. Такое поведение реализовано в host_client. host_client должен работать в другом терминале до того, как вы запустите свою программу на ESP Rust Board. host_client должен вывести примерно следующее:
Setting new color: rgb(1,196,156)
Setting new color: rgb(182,190,128)
Board says hi!
✅ В цикле в конце функции main публикуйте температуру платы в temperature_data_topic(uuid) каждую секунду. Проверьте это также с помощью host_client:
Setting new color: rgb(218,157,124)
Board temperature: 33.29°C
Setting new color: rgb(45,88,22)
Board temperature: 33.32°C
Установка соединения
Соединениями управляет экземпляр esp_idf_svc::mqtt::client::EspMqttClient.
Его создают, передавая:
- URL брокера, который при необходимости содержит учётные данные;
- конфигурацию типа
esp_idf_svc::mqtt::client::MqttClientConfiguration; - замыкание-обработчик, похожее на то, что используется в упражнении с HTTP-сервером.
#![allow(unused)]
fn main() {
let mut client = EspMqttClient::new(broker_url,
&mqtt_config,
move |message_event| {
// ... ваш код обработчика — пока оставьте пустым
// функциональность добавим позже в этой главе
})?;
}
Вспомогательные инструменты и крейты
Чтобы логировать показания датчиков, которые отправляет плата, в intro/mqtt/host_client есть вспомогательный клиент. Он подписан на топик температуры.
Крейт mqtt_messages (находится в common/lib) помогает работать с сообщениями, подписками и топиками:
Функции для формирования строк топиков
color_topic(uuid)— создаёт топик, в который публикуются цвета для платы.
hello_topic(uuid)— тестовый топик для первоначальной проверки успешного подключения;temperature_data_topic(uuid)— создаёт полную строку топика «temperature».
Кодирование и декодирование полезной нагрузки сообщений
Температура платы типа f32 преобразуется в четыре байта в порядке «big-endian» с помощью temp.to_be_bytes().
#![allow(unused)]
fn main() {
// температура
let temperature_data = &temp.to_be_bytes() as &[u8]; // плата
let decoded_temperature = f32::from_be_bytes(temperature_data); // компьютер разработчика
}
Публикация и подписка
EspMqttClient также отвечает за публикацию сообщений в заданный топик.
Функция publish принимает параметр retain, который показывает, нужно ли доставлять это сообщение и клиентам, подключившимся после его публикации.
#![allow(unused)]
fn main() {
let publish_topic = /* ... */;
let payload: &[u8] = /* ... */ ;
client.publish(publish_topic, QoS::AtLeastOnce, false, payload)?;
}
Устранение неполадок
error: expected expression, found .при сборке примера клиента: обновите установленный stable Rust до версии 1.58 или новее.- Не видно сообщений MQTT? Убедитесь, что все клиенты (плата и компьютер разработчика) используют один и тот же UUID (его видно в выводе логов).
- Убедитесь, что файл
cfg.tomlнастроен правильно. В начале работыexample-clientвыводитdbg!()с конфигурациейmqtt. Он должен вывести содержимое вашегоcfg.toml. error: expected expression, found .при запуске host-клиента решается командойrustup update.
Упражнение MQTT: получение команд для светодиода
✅ Подпишитесь на color_topic(uuid).
✅ Запустите host_client параллельно, в отдельном терминале. host_client публикует color светодиода платы примерно раз в секунду.
✅ Проверьте, что подписка работает, выведя в лог информацию, полученную через топик.
✅ Реагируйте на команды светодиода: установите полученный цвет на плате с помощью led.set_pixel(/* received color here */).
intro/mqtt/exercise/solution/solution_publ_rcv.rs содержит решение. Запустить его можно командой:
cargo run --example solution_publ_rcv
Кодирование и декодирование полезной нагрузки сообщений
Команды светодиода платы состоят из трёх байтов, которые обозначают красный, зелёный и синий.
enum ColorDataсодержит топикcolor_topic(uuid)иBoardLed;- поле
data()структурыEspMqttMessageможно преобразовать с помощьюtry_from(). Сначала сообщение нужно привести к срезу:let message_data: &[u8] = &message.data();
#![allow(unused)]
fn main() {
// команда RGB-светодиода
if let Ok(ColorData::BoardLed(color)) = ColorData::try_from(message_data) { /* установить новый цвет здесь */ }
}
Публикация и подписка
EspMqttClient отвечает не только за публикацию, но и за подписку на топики.
#![allow(unused)]
fn main() {
let subscribe_topic = /* ... */;
client.subscribe(subscribe_topic, QoS::AtLeastOnce)
}
Обработка входящих сообщений
Параметр message_event в замыкании-обработчике имеет тип EspMqttEvent, у которого есть метод payload() для доступа к EventPayload.
Поскольку нас интересуют только успешно полученные сообщения:
#![allow(unused)]
fn main() {
let mut client =
EspMqttClient::new_cb(
&broker_url,
&mqtt_config,
move |message_event| match message_event.payload() {
Received { data, details, .. } => process_message(data, details, &mut led),
Error(e) => warn!("Received error from MQTT: {:?}", e),
_ => info!("Received from MQTT: {:?}", message_event.payload()),
},
)?;
}
В обработчике нужно обрабатывать сообщения Complete.
💡 Используйте Rust Analyzer, чтобы сгенерировать недостающие ветки match, или обработайте любой другой тип ответа, выведя его через info!().
#![allow(unused)]
fn main() {
fn process_message(data: &[u8], details: Details, led: &mut WS2812RMT) {
match details {
Complete => {
info!("{:?}", data);
let message_data: &[u8] = data;
if let Ok(ColorData::BoardLed(color)) = ColorData::try_from(message_data) {
info!("{}", color);
if let Err(e) = led.set_pixel(color) {
error!("Could not set board LED: {:?}", e)
};
}
}
_ => {}
}
}
}
💡 Используйте логгер, чтобы увидеть, что вы получаете, например info!("{}", color); или dbg!(color).
Дополнительные задания
Реализуйте MQTT с иерархическими топиками
✅ Выполняйте это, если всё остальное уже готово. Полного решения мы не даём: задача в том, чтобы проверить, как далеко вы продвинетесь самостоятельно.
Посмотрите common/lib/mqtt-messages:
✅ Реализуйте ту же процедуру, но с использованием иерархии MQTT. Подписывайтесь на все сообщения «command», комбинируя cmd_topic_fragment(uuid) с завершающим шаблоном #.
✅ Используйте enum Command вместо enum ColorData. enum Command представляет все возможные команды (здесь — только BoardLed).
✅ RawCommandData хранит последнюю часть топика сообщения (например, board_led в a-uuid/command/board_led). Его можно преобразовать в Command с помощью try_from.
#![allow(unused)]
fn main() {
// команда RGB-светодиода
let raw = RawCommandData {
path: command,
data: message.data(),
};
}
Посмотрите host-client:
✅ Нужно заменить color на command. Например, так:
#![allow(unused)]
fn main() {
let command = Command::BoardLed(color)
}
Другие задания
✅ Используйте serde_json, чтобы кодировать и декодировать данные сообщений в JSON.
✅ Отправьте с клиента-хоста несколько сообщений с большой полезной нагрузкой и обработайте их на микроконтроллере. Большие сообщения доставляются по частям вместо Details::Complete:
#![allow(unused)]
fn main() {
InitialChunk(chunk_info) => { /* первый фрагмент */},
SubsequentChunk(chunk_data) => { /* все последующие фрагменты */ }
}
💡 Не нужно различать входящие фрагменты по ID сообщения: в любой момент времени в пути может находиться не более одного сообщения.
Устранение неполадок
error: expected expression, found .при сборке host-клиента: обновите stable Rust до версии 1.58 или новее.- Не видно сообщений MQTT? Убедитесь, что все клиенты (плата и компьютер разработчика) используют один и тот же UUID (его видно в выводе логов).
Продвинутый практикум
В этом практикуме мы углубимся в темы, которые относятся только к встраиваемым системам и/или близки к железу, в первую очередь к низкоуровневому вводу-выводу. В отличие от первой части, мы не будем ограничиваться высокоуровневыми абстракциями, которые, например, скрывают настройку выводов. Вместо этого вы научитесь настраивать их сами. Также вы научитесь писать напрямую в регистры и выяснять, какой регистр для этого нужен. Вопросы владения и безопасности памяти мы разберём в контексте упражнений.
Эта часть состоит из трёх упражнений.
В первом вы научитесь обрабатывать прерывание от кнопки, во втором — считывать показания датчиков по шине I²C. Когда вы поработаете с подготовленными нами драйверами, вы научитесь писать собственные. Это необходимый навык, ведь драйверы для Rust обычно не предоставляют производители.
Подготовка
Пройдите главу подготовки, чтобы подготовиться к этому практикуму.
Справочник
Если вы впервые занимаетесь встраиваемым программированием, прочитайте наш справочник, где в простой форме объясняются некоторые термины.
Низкоуровневый ввод-вывод: как работать с регистрами
Есть два способа писать прошивку для ESP32-C3:
- первый — bare-metal, с использованием только
[no_std]Rust; - второй — с использованием
[std]Rust и C-биндингов к ESP-IDF.
[no_std]Rust — это Rust без стандартной библиотеки, только с core-библиотекой. Core — подмножество стандартной библиотеки, которое не зависит от наличия операционной системы.
Как устроены экосистемы?
[std] Rust и ESP-IDF
Этот путь опирается на C-биндинги к ESP-IDF. По этому пути можно использовать стандартную библиотеку Rust, поскольку у нас есть операционная система: ESP-IDF, основанная на FreeRTOS. Возможность пользоваться стандартной библиотекой даёт преимущества: можно использовать любые типы, независимо от того, размещены ли они на стеке или в куче. Можно использовать потоки, Mutex и другие примитивы синхронизации.
ESP-IDF в основном написан на C и открывается в Rust по классической схеме разделения на крейты:
- крейт
sys, предоставляющий собственноunsafe-биндинги (esp-idf-sys); - крейт более высокого уровня с безопасными и удобными абстракциями Rust (esp-idf-svc).
Последняя часть головоломки — низкоуровневый доступ к железу, который снова предоставляется в разделённом виде:
esp-idf-halреализует не зависящие от железа трейтыembedded-hal, такие как аналого-цифровое преобразование, цифровые выводы ввода-вывода или связь по SPI. Как следует из названия, он тоже использует ESP-IDF в качестве основы.
Подробнее — в главе об экосистеме книги The Rust on ESP Book.
На сегодняшний день этот путь даёт наибольшие возможности на чипах Espressif, если вы хотите использовать Rust. Весь материал этого курса основан на этом подходе.
Как записывать значения в регистры в этой экосистеме, мы рассмотрим в контексте упражнения с прерываниями.
Bare metal Rust с [no_std]
Как следует из названия bare metal, операционная система не используется. Поэтому мы не можем пользоваться возможностями языка, которые на ней основаны. Core-библиотека — это подмножество стандартной библиотеки, которое исключает такие возможности, как типы, размещаемые в куче, и потоки. Код, использующий только core-библиотеку, помечается #[no_std]. Код #[no_std] всегда может работать в окружении std, но обратное неверно.
В Rust связь между регистрами и кодом устроена так:
Регистры и их поля в устройстве описываются в файлах System View Description (SVD). С помощью svd2rust по этим файлам генерируются Peripheral Access Crates (PAC). PAC представляет собой тонкую обёртку над различными регистрами с отображением в память, определёнными для конкретной модели используемого микроконтроллера.
Хотя прошивку можно написать и только с PAC, часть кода получилась бы небезопасной или неудобной, так как PAC даёт лишь самый базовый доступ к периферии микроконтроллера. Поэтому есть ещё один слой — Hardware Abstraction Layer (HAL). HAL предоставляет более удобный API для чипа и часто реализует общие трейты, определённые в универсальном embedded-hal.
Микроконтроллеры обычно припаяны к какой-нибудь печатной плате (или просто плате), которая определяет подключения к каждому выводу. Для конкретной платы может быть написан Board Support Crate (BSC, также известный как Board Support Package или BSP). Он добавляет ещё один слой абстракции и может, например, предоставлять API для различных датчиков и светодиодов на плате, без необходимости знать, к каким выводам микроконтроллера они подключены.
Мы напишем частичный драйвер датчика в таком подходе, так как драйверы должны быть платформонезависимыми.
I²C
Введение
Inter-Integrated Circuit, обычно сокращаемый до I²C или I2C, — это последовательный протокол, который позволяет нескольким периферийным (или ведомым) микросхемам обмениваться данными с одной или несколькими управляющими (или ведущими) микросхемами. К одной шине I²C может быть подключено много устройств, а сообщение можно отправить конкретному устройству, указав его адрес I²C. Протоколу нужны две сигнальные линии, и он применяется только для связи на небольших расстояниях внутри одного устройства.
Одна линия — для данных (SDA), другая — для тактового сигнала (SCL). По умолчанию линии подтянуты к высокому уровню резисторами, установленными где-то на шине. Любое устройство на шине (или даже несколько устройств одновременно) может «притянуть» одну или обе линии к низкому уровню. Поэтому, если два устройства одновременно попытаются говорить по шине, ничего не сломается: сообщения просто искажаются, и это можно обнаружить.
I²C-транзакция состоит из одного или нескольких сообщений. Каждое сообщение состоит из стартового символа, нескольких слов и, наконец, стопового символа (или ещё одного стартового символа, если за ним следует другое сообщение). Каждое слово — это восемь бит, за которыми следует бит ACK (0) или NACK (1), который отправляет получатель, чтобы показать, было ли слово получено и понято правильно. Первое слово содержит 7-битный адрес устройства, которому адресовано сообщение, а также бит, показывающий, будет ли устройство читаться или записываться. Если на шине нет устройства с таким адресом, за первым словом естественным образом последует NACK, потому что никакое устройство не притянет линию SDA к низкому уровню, чтобы сформировать бит ACK. Так вы поймёте, что устройства нет.
Тактовая частота линии SCL обычно составляет 400 кГц, но поддерживаются и более низкие, и более высокие скорости (стандартные скорости: 100 кГц, 400 кГц, 1 МГц). В нашем упражнении используется 400 кГц ( <MasterConfig as Default>::default().baudrate(400.kHz().into())).
Чтобы прочитать три байта из EEPROM, последовательность будет примерно такой:
| Шаг | Контроллер отправляет | Периферия отправляет |
|---|---|---|
| 1. | START | |
| 2. | Адрес устройства + W | |
| 3. | ACK | |
| 4. | Старший байт адреса EEPROM | |
| 5. | ACK | |
| 6. | Младший байт адреса EEPROM | |
| 7. | ACK | |
| 8. | START | |
| 9. | Адрес устройства + R | |
| 10. | ACK | |
| 11. | Байт данных по адресу EEPROM | |
| 12. | ACK | |
| 13. | Байт данных по адресу EEPROM + 1 | |
| 14. | ACK | |
| 15. | Байт данных по адресу EEPROM + 2 | |
| 16. | NAK (т. е. конец чтения) | |
| 17. | STOP |
Изображение сигналов I²C
Диаграмма последовательности передачи данных по шине I²C:
- S — условие START;
- P — условие STOP;
- B1 … BN — передача одного бита;
- изменения SDA допускаются, когда SCL низкий (синий), иначе будет сформировано условие START или STOP.
Источник и подробности: Википедия.
Чтение датчиков
Упражнение: чтение датчиков I²C
В этом упражнении вы научитесь считывать показания датчиков на шине I²C.
На Rust ESP Board есть два датчика, которые можно считывать по шине I²C:
| Периферия | Модель | Документация | Крейт | Адрес |
|---|---|---|---|---|
| IMU | ICM-42670-P | Даташит | Ссылка | 0x68 |
| Температура и влажность | SHTC3 | Даташит | Ссылка | 0x70 |
Задача — использовать готовый драйвер из crates.io, чтобы считать показания датчика температуры и влажности по I²C. После этого второй датчик будет считываться по той же шине I²C с помощью shared-bus.
Часть 1: чтение температуры и влажности
Создайте экземпляр датчика влажности SHTC3 и каждые 600 мс считывайте и выводите значения влажности и температуры.
i2c-sensor-reading/examples/part_1.rs содержит рабочее решение части 1. Чтобы запустить решение части 1:
cargo run --example part_1
i2c-sensor-reading/src/main.rs содержит заготовку кода, в которой уже есть импорты, нужные для этой части.
Шаги:
✅ Перейдите в папку i2c-sensor-reading/ и откройте соответствующую документацию командой:
cargo doc --open
✅ Определите два вывода: один как SDA, другой как SCL.
| Сигнал | GPIO |
|---|---|
| SDA | GPIO10 |
| SCL | GPIO8 |
✅ Создайте экземпляр периферии I²C с помощью сгенерированной документации. Используйте частоту 400 кГц.
✅ Используя крейт драйвера shtcx, создайте экземпляр датчика SHTC3, передав в него экземпляр I²C. За подробностями обращайтесь к документации.
✅ Чтобы проверить, что к датчику обращаются правильно, считайте его идентификатор устройства и выведите значение.
Ожидаемый вывод:
Device ID SHTC3: 71
✅ Выполните измерение, считайте значения датчика и выведите их. Методы датчика описаны в документации.
Ожидаемый вывод:
TEMP: [локальная температура] °C
HUM: [локальная влажность] %
❗ Некоторым датчикам нужно время между измерением и чтением значения. ❗ Следите за единицами измерения!
💡 Есть методы, которые переводят значения датчика в нужные единицы.
Часть 2: чтение данных акселерометра
С помощью менеджера шины реализуйте второй датчик. Считайте его значения и выведите значения обоих датчиков.
Продолжайте со своим решением части 1. Или можно начать с готового частичного решения части 1: i2c-sensor-reading/examples/part_1.rs.
i2c-sensor-reading/examples/part_2.rs содержит рабочее решение части 2. Если нужна помощь, можно посмотреть его, запустив:
cargo run --example part_2
Шаги
✅ Импортируйте крейт драйвера для ICM42670p.
#![allow(unused)]
fn main() {
use icm42670::{Address, Icm42670, PowerMode as imuPowerMode};
}
✅ Создайте экземпляр датчика.
✅ Почему передача одного и того же экземпляра I²C двум датчикам не работает, хотя оба находятся на одной шине I²C?
Ответ
Это вопрос владения. Каждое место в памяти должно кому-то принадлежать. Если мы передадим шину I²C в SHTC3, датчик станет её владельцем, и ею не сможет владеть другой датчик. Заимствовать её тоже нельзя, потому что шина I²C должна быть изменяемой: оба датчика должны иметь возможность её менять. Мы решаем эту проблему, вводя менеджер шины, который создаёт несколько прокси шины I²C. Эти прокси затем могут принадлежать соответствующим датчикам.
✅ Импортируйте крейт менеджера шины.
#![allow(unused)]
fn main() {
use shared_bus::BusManagerSimple;
}
✅ Создайте экземпляр простого менеджера шины. Создайте два прокси и передавайте их датчикам вместо исходного экземпляра I²C.
✅ Считайте и выведите идентификатор устройства с обоих датчиков.
Ожидаемый вывод:
Device ID SHTC3: 71
Device ID ICM42670p: 96
✅ Запустите ICM42670p в режиме низкого шума (low noise mode).
✅ Считайте значения гироскопа и выведите их с двумя знаками после запятой вместе со значениями температуры и влажности.
Ожидаемый вывод:
GYRO: X: 0.00 Y: 0.00 Z: 0:00
TEMP: [локальная температура] °C
HUM: [локальная влажность] %
Упражнение: драйвер I²C, простая версия
Мы не будем писать драйвер целиком, только первый шаг: «hello world» написания драйверов, то есть чтение идентификатора устройства датчика. Эта версия называется простой, потому что мы объясняем фрагменты кода, а вам нужно лишь скопировать их в нужное место. Используйте эту версию, если у вас мало опыта работы с Rust, если этот практикум — ваш первый во встраиваемой области или если сложная версия оказалась для вас слишком трудной. Вы можете работать в одном и том же файле с любой из версий.
i2c-driver/src/icm42670p.rs — это текст с пропусками, представляющий собой очень простой драйвер IMU-датчика I²C. Задача — дописать файл так, чтобы запуск main.rs выводил в лог идентификатор устройства драйвера.
i2c-driver/src/icm42670p_solution.rs содержит решение этого упражнения. Чтобы его запустить, нужно поменять импорты в main.rs и lib.rs. Импорты уже есть: достаточно закомментировать текущие импорты и раскомментировать решения, отмеченные комментариями в строках.
API драйвера
Экземпляр датчика
Чтобы работать с периферийным датчиком, сначала нужно получить его экземпляр. Датчик представлен структурой, которая содержит и адрес устройства, и объект, представляющий саму шину I²C. Для этого используются трейты, определённые в крейте embedded-hal. Структура публичная, потому что к ней нужен доступ извне крейта, но её поля приватные.
#![allow(unused)]
fn main() {
#[derive(Debug)]
pub struct ICM42670P<I2C> {
// The concrete I²C device implementation.
i2c: I2C,
// Device address
address: DeviceAddr,
}
}
Добавляем блок impl, который будет содержать все методы, доступные для экземпляра датчика. В нём также определяется обработка ошибок. В этом же блоке реализуем метод создания экземпляра. Методы могут быть публичными или приватными. Этот метод должен быть доступен извне, поэтому он помечен как pub. Обратите внимание: при такой записи экземпляр датчика становится владельцем шины I²C.
#![allow(unused)]
fn main() {
impl<I2C, E> ICM42670P<I2C>
where
I2C: i2c::WriteRead<Error = E> + i2c::Write<Error = E>,
{
/// Creates a new instance of the sensor, taking ownership of the i2c peripheral.
pub fn new(i2c: I2C, address: DeviceAddr) -> Result<Self, E> {
Ok(Self { i2c, address })
}
// ...
}
Адрес устройства
- Адреса устройства доступны в коде:
#![allow(unused)]
fn main() {
pub enum DeviceAddr {
/// 0x68
AD0 = 0b110_1000,
/// 0x69
AD1 = 0b110_1001,
}
}
- У этого устройства I²C два возможных адреса:
0x68и0x69. Мы указываем устройству, какой адрес использовать, подавая на выводAP_AD0либо0V, либо3.3V. При0Vустройство отвечает на адрес0x68, при3.3V— на0x69. Поэтому выводAP_AD0можно считать однобитным входом, который задаёт младший бит адреса устройства. Подробнее — в даташите, раздел 9.3.
Представление регистров
Регистры датчика представлены перечислениями. Каждый вариант имеет адрес регистра в качестве значения. Тип Register реализует метод, который возвращает адрес варианта.
#![allow(unused)]
fn main() {
#[derive(Clone, Copy)]
pub enum Register {
WhoAmI = 0x75,
}
impl Register {
fn address(&self) -> u8 {
*self as u8
}
}
}
read_register() и write_register()
Определим методы чтения и записи на основе методов, которые предоставляет крейт embedded-hal. Они служат вспомогательными средствами для более конкретных методов и абстракцией, адаптированной под датчик с 8-битными регистрами. Обратите внимание, что метод read_register() основан на методе write_read(). Причина — в особенностях протокола I²C: сначала нужно записать по шине команду, которая указывает, из какого регистра мы хотим читать. Вспомогательные методы могут оставаться приватными, так как им не нужен доступ извне крейта.
#![allow(unused)]
fn main() {
impl<I2C, E> ICM42670P<I2C>
where
I2C: i2c::WriteRead<Error = E> + i2c::Write<Error = E>,
{
/// Creates a new instance of the sensor, taking ownership of the i2c peripheral.
pub fn new(i2c: I2C, address: DeviceAddr) -> Result<Self, E> {
Ok(Self { i2c, address })
}
// ...
/// Writes into a register
// This method is not public as it is only needed inside this file.
#[allow(unused)]
fn write_register(&mut self, register: Register, value: u8) -> Result<(), E> {
let byte = value;
self.i2c
.write(self.address as u8, &[register.address(), byte])
}
/// Reads a register using a `write_read` method.
// This method is not public as it is only needed inside this file.
fn read_register(&mut self, register: Register) -> Result<u8, E> {
let mut data = [0];
self.i2c
.write_read(self.address as u8, &[register.address()], &mut data)?;
Ok(u8::from_le_bytes(data))
}
}
✅ Реализуйте публичный метод, который читает регистр WhoAmI по адресу 0x75. Используйте метод read_register(), описанный выше.
✅ Необязательно: реализуйте дополнительные методы, которые добавляют драйверу возможности. Адреса и описание соответствующих регистров смотрите в документации. 💡 Несколько идей:
- включение гироскопа или акселерометра;
- запуск измерений;
- чтение измерений.
🔎 Общие сведения о регистрах периферии
Регистры могут иметь разный смысл. По сути, это ячейка, которая может хранить значение.
В данном случае мы работаем с внешним устройством (поскольку это датчик, даже если он на той же печатной плате). Оно адресуется по I2C, и мы читаем и записываем его адреса регистров. Каждый адрес указывает на уникальную ячейку, которая содержит какую-то информацию. В данном случае нам нужен адрес ячейки, в которой хранится текущая температура, измеренная датчиком.
Карту регистров ICM-42670 можно найти в разделе 14, если вы хотите получить из этого датчика другие интересные данные.
Написание драйвера I²C: сложная версия
Мы не будем писать драйвер целиком, только первый шаг: «hello world» написания драйверов, то есть чтение идентификатора устройства датчика. Эта версия называется сложной, потому что содержимое методов придётся придумать самим, а информацию искать в embedded-hal и даташитах. Вы можете работать в одном и том же файле с любой из версий.
i2c-driver/src/icm42670p.rs — это текст с пропусками, представляющий собой очень простой драйвер IMU-датчика I²C. Задача — дописать файл так, чтобы запуск main.rs выводил в лог идентификатор устройства драйвера.
i2c-driver/src/icm42670p_solution.rs содержит решение этого упражнения. Чтобы его запустить, нужно поменять импорты в main.rs и lib.rs. Импорты уже есть: достаточно закомментировать текущие импорты и раскомментировать решения, отмеченные комментариями в строках.
API драйвера
Экземпляр датчика
✅ Создайте структуру, которая представляет датчик. У неё два поля: одно — адрес устройства датчика, другое — сама шина I²C. Это делается с помощью трейтов, определённых в крейте embedded-hal. Структура публичная, так как к ней нужен доступ извне крейта, но её поля приватные.
✅ Реализуйте метод создания экземпляра в блоке impl. Этот метод должен быть доступен извне, поэтому помечен как pub. Метод забирает владение шиной I²C и создаёт экземпляр структуры, которую вы определили ранее.
Адрес устройства
✅ У этого устройства I²C два возможных адреса. Найдите их в даташите, раздел 9.3.
🔎 Мы указываем устройству, какой адрес использовать, подавая на вывод AP_AD0 либо 0V, либо 3.3V. При 0V устройство отвечает на адрес 0x68, при 3.3V — на 0x69. Поэтому вывод AP_AD0 можно считать однобитным входом, который задаёт младший бит адреса устройства.
✅ Создайте перечисление, которое представляет оба варианта адреса. Значения вариантов должны быть записаны в двоичном виде.
Представление регистров
✅ Создайте перечисление, которое представляет регистры датчика. Каждый вариант имеет адрес регистра в качестве значения. Пока вам нужен только регистр WhoAmI. Его адрес найдите в даташите.
✅ Реализуйте метод, который возвращает адрес варианта как u8.
read_register() и write_register()
✅ Изучите функции write и write_read в embedded-hal. Почему это write_read, а не просто read?
Ответ
Причина — в особенностях протокола I²C. Сначала нужно записать по шине команду, которая указывает, из какого регистра мы хотим читать.✅ Определите методы read_register и write_register для экземпляра датчика. Используйте методы, которые предоставляет крейт embedded-hal. Они служат вспомогательными средствами для более конкретных методов и абстракцией под датчик с 8-битными регистрами. Это значит, что записываемые и читаемые данные — беззнаковые 8-битные целые. Вспомогательные методы могут оставаться приватными, так как им не нужен доступ извне крейта.
✅ Реализуйте публичный метод, который читает регистр WhoAmI по адресу 0x75. Используйте метод read_register(), описанный выше.
✅ Необязательно: реализуйте дополнительные методы, которые добавляют драйверу возможности. Адреса и описание соответствующих регистров смотрите в документации. 💡 Несколько идей:
- включение гироскопа или акселерометра;
- запуск измерений;
- чтение измерений.
🔎 Общие сведения о регистрах периферии
- Регистры — это небольшие ячейки памяти, к которым процессор имеет прямой доступ. Регистры датчика имеют размер 8 бит.
- К ним можно обращаться по адресу.
- Карты регистров можно найти в разделе 14.
- Составление значения из старшего и младшего байтов (MSB и LSB) делается сдвигом значения MSB и операцией OR со значением LSB.
#![allow(unused)]
fn main() {
let GYRO_DATA_X: i16 = ((GYRO_DATA_X1 as i16) << 8) | GYRO_DATA_X0 as i16;
}
Прерывания
Прерывание — это запрос к процессору приостановить текущее выполнение, чтобы событие было обработано вовремя. Если запрос принят, процессор приостанавливает текущую работу, сохраняет своё состояние и выполняет функцию, которая называется обработчиком прерывания, чтобы обработать событие. Прерывания часто используются аппаратными устройствами, чтобы сообщить об изменениях электронного или физического состояния, которые требуют срочного внимания, например о нажатии кнопки.
То, что обработчики прерываний могут вызываться в любой момент, создаёт в embedded Rust сложность: нужна статически выделенная изменяемая память, на которую могут ссылаться и обработчик прерывания, и основной код, причём эта память должна быть всегда доступна.
Блоки unsafe {}
В этом коде много блоков unsafe {}. Как общее правило, unsafe не означает, что содержащийся в нём код небезопасен с точки зрения памяти. Это означает, что Rust не может дать гарантии безопасности в этом месте и что ответственность за безопасность памяти лежит на программисте. Например, вызов C-биндингов сам по себе небезопасен, поскольку Rust не может дать никаких гарантий безопасности для лежащего в основе C-кода.
Создание обработчика прерываний
Цель этого упражнения — обработать прерывание, которое срабатывает при нажатии кнопки BOOT.
Заготовку кода для этого упражнения можно найти в advanced/button-interrupt/src/main.rs.
Решение упражнения находится в advanced/button-interrupt/examples/solution.rs. Запустить его можно командой:
cargo run --example solution
✅ Задания
- Настройте кнопку BOOT (GPIO9) с помощью структуры
PinDriverсо следующими параметрами:- режим входа (Input mode);
- подтяжка к питанию (Pull up);
- прерывание по положительному фронту (Interrupt on positive edge).
- Создайте новое уведомление (notification) и notifier:
- см. документацию
hal::task::notification.
- см. документацию
- В блоке
unsafeсоздайте подписку и её функцию обратного вызова.- см.
PinDriver::subscribeиtask::notify_and_yield; - причины, по которым это
unsafe:- функция обратного вызова выполняется в ISR (Interrupt Service Routine), поэтому не следует вызывать в ней блокирующие функции; это относится к STD,
libcи API FreeRTOS (за исключением нескольких разрешённых); - замыкание обратного вызова захватывает своё окружение, и в него можно передать статические переменные. Захваченные переменные должны переживать подписку. Можно также использовать нестатические переменные, но для этого нужна дополнительная осторожность; подробнее см. документацию
esp_idf_hal::gpio::PinDriver::subscribe_nonstatic.
- функция обратного вызова выполняется в ISR (Interrupt Service Routine), поэтому не следует вызывать в ней блокирующие функции; это относится к STD,
- см.
- В цикле включите прерывание и дождитесь уведомления:
- прерывание нужно включать после каждого полученного уведомления, из контекста не-ISR;
- для ожидания можно использовать
esp_idf_svc::hal::delay::BLOCK.
- Запустите программу, нажмите кнопку
BOOTи посмотрите, как это работает!
🔎 В этом упражнении мы используем уведомления, которые передают только последнее значение. Если прерывание сработало несколько раз до того, как прочитано значение уведомления, вы сможете прочитать только последнее. Очереди, напротив, позволяют получить несколько значений. Подробнее см. esp_idf_hal::task::queue::Queue.
Случайный цвет светодиода по нажатию кнопки
✅ Измените код так, чтобы RGB-светодиод менял цвет на другой случайный при каждом нажатии кнопки. Светодиод не должен гаснуть или менять цвет, если кнопку некоторое время не нажимают.
Продолжите, добавив код к предыдущему решению или к коду из advanced/button-interrupt/src/main.rs.
Решение упражнения находится в advanced/button-interrupt/examples/solution_led.rs. Запустить его можно командой:
cargo run --example solution_led
💡 Подсказки
- Нужные импорты уже есть. Если выполнить
cargo doc --open, вы получите полезную документацию по светодиоду. - Тип светодиода — WS2812RMT.
- Это программируемый RGB-светодиод. Это значит, что нет отдельных выводов для красного, зелёного и синего, а нужно создать его экземпляр, чтобы отправлять ему значения типа
RGB8через метод. - На плате есть аппаратный генератор случайных чисел. Его можно вызвать через
esp_random(). - Вызов функций из
esp-idf-svc::sysс точки зрения Rust небезопасен и требует блокаunsafe(). Можно считать, что эти функции безопасны в использовании, поэтому других мер не требуется.
Пошаговое руководство по решению
-
Инициализируйте периферию светодиода и включите его с произвольным значением, просто чтобы убедиться, что он работает.
#![allow(unused)] fn main() { let mut led = WS2812RMT::new(peripherals.pins.gpio2, peripherals.rmt.channel0)?; led.set_pixel(RGB8::new(20, 0, 20)).unwrap(); // Удалите эту строку после того, как попробуете один раз } -
Включайте светодиод только при нажатии кнопки. Пока это можно сделать, добавив следующую строку после сообщения о нажатии кнопки:
#![allow(unused)] fn main() { led.set_pixel(arbitrary_color)?; } -
Создайте случайные RGB-значения, вызвав
esp_random().- Эта функция
unsafe. - Она возвращает
u32, поэтому её нужно привести кu8.
#![allow(unused)] fn main() { unsafe { //... 1 => { let r = esp_random() as u8; let g = esp_random() as u8; let b = esp_random() as u8; let color = RGB8::new(r, g, b); led.set_pixel(color)?; }, _ => {}, } - Эта функция
-
Необязательно: если вы собираетесь переиспользовать этот код в другом месте, имеет смысл вынести его в отдельную функцию. Заодно можно чётко определить, какие части кода должны находиться в блоках
unsafe.
#![allow(unused)]
fn main() {
// ...
loop {
// Enable interrupt and wait for new notificaton
button.enable_interrupt()?;
notification.wait(esp_idf_svc::hal::delay::BLOCK);
println!("Button pressed!");
// Generates random rgb values and sets them in the led.
random_light(&mut led);
}
// ...
fn random_light(led: &mut WS2812RMT) {
let mut color = RGB8::new(0, 0, 0);
unsafe {
let r = esp_random() as u8;
let g = esp_random() as u8;
let b = esp_random() as u8;
color = RGB8::new(r, g, b);
}
led.set_pixel(color).unwrap();
}
}
Справочник
GPIO
GPIO — сокращение от General Purpose Input Output (универсальный вход-выход). GPIO — это цифровые (иногда аналоговые) сигнальные выводы, которые можно использовать как интерфейсы к другим системам или устройствам. Каждый вывод может находиться в разных состояниях, но при включении питания или после сброса системы у него будет состояние по умолчанию (обычно безобидное, например цифровой вход). Затем мы можем написать программу, которая переведёт вывод в нужное нам состояние.
Познакомимся с несколькими понятиями, связанными с GPIO:
Конфигурации выводов
GPIO можно настроить одним из нескольких способов. Доступные варианты зависят от конструкции чипа, но обычно включают:
Floating (плавающий): плавающий вывод не подключён ни к VCC, ни к земле. Он просто принимает то напряжение, которое на него подано. Однако учтите: схема должна подтягивать вывод внешним образом к низкому или высокому уровню, потому что КМОП-устройства (например, микроконтроллеры) могут работать некорректно, если вывод дольше нескольких микросекунд находится выше «порога низкого уровня» Vtl, но ниже «порога высокого уровня» Vth.
Push-Pull-Output (двухтактный выход): вывод, настроенный как двухтактный выход, может либо выставлять на себя высокое напряжение (то есть подключаться к VCC), либо низкое (подключаться к земле). Это удобно для светодиодов, пищалок и других устройств, которые потребляют немного энергии.
Open-Drain-Output (выход с открытым стоком): выходы с открытым стоком переключаются между состояниями «отключено» и «подключено к земле». Предполагается, что внешний резистор слабо подтягивает линию к VCC. Такой выход позволяет соединять несколько устройств вместе: линия находится в «низком» состоянии, если хотя бы одно из подключённых устройств притягивает её к земле. Если два или больше устройств притягивают её к земле одновременно, ничего не повреждается (соединение земли с землёй безопасно). Если ни одно из них не притягивает линию вниз, резистор по умолчанию подтянет её к высокому уровню.
Floating-Input (плавающий вход): вывод, на котором внешнее напряжение можно прочитать программно как 1 (обычно если напряжение выше некоторого порога) или 0 (если нет). Для этого состояния действуют те же предупреждения, что и для «Floating».
Pull-Up-Input (вход с подтяжкой вверх): как Floating-Input, но внутренний «pull-up» резистор слабо подтягивает линию к VCC, когда ничто внешнее не тянет её к земле. Удобно для чтения кнопок и других переключателей, так как не нужен внешний резистор.
Активный высокий и низкий уровни
Цифровой сигнал может находиться в двух состояниях: high (высокий) и low (низкий). Обычно это выражается разностью напряжений между сигналом и землёй. Какой уровень напряжения соответствует какому логическому состоянию, определяется произвольно, поэтому активным может быть как high, так и low.
Например, у активного высокого вывода есть напряжение, когда логический уровень активен, а у активного низкого вывода напряжение есть, когда логический уровень неактивен.
В embedded Rust абстракции показывают логический уровень, а не уровень напряжения. Поэтому, если активный низкий вывод подключён к светодиоду, чтобы светодиод загорелся, его нужно перевести в неактивное состояние.
Выбор чипа (Chip Select)
Chip Select — двоичный сигнал, который может включать или выключать другое устройство, полностью или частично. Обычно это сигнальная линия, подключённая к GPIO. Её часто используют, чтобы несколько устройств могли работать на одной шине SPI: каждое устройство «слушает» шину, только когда его линия Chip Select активна.
Bit banging
Для протоколов вроде I2C или SPI обычно используется периферия внутри микроконтроллера, которая преобразует данные для передачи в сигналы. Иногда, например если микроконтроллер не поддерживает протокол или используется нестандартная его форма, нужно написать программу, которая превращает данные в сигналы вручную. Это называется bit-banging.